@allocator-one/harness-cli 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +540 -23
- package/dist/{chunk-F2YVTB2A.js → chunk-I2S2JVCQ.js} +2024 -127
- package/dist/cli.js +419 -51
- package/dist/{server-75QJASTY.js → server-O7CX2ZCC.js} +210 -15
- package/package.json +2 -1
- package/spec/openapi.json +12302 -3927
|
@@ -20,10 +20,12 @@ var UpgradeRequiredError = class extends HttpError {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
var TransportError = class extends Error {
|
|
23
|
-
constructor(message) {
|
|
23
|
+
constructor(message, code = null) {
|
|
24
24
|
super(message);
|
|
25
|
+
this.code = code;
|
|
25
26
|
this.name = "TransportError";
|
|
26
27
|
}
|
|
28
|
+
code;
|
|
27
29
|
};
|
|
28
30
|
var MalformedResponseError = class extends TransportError {
|
|
29
31
|
constructor(message) {
|
|
@@ -56,6 +58,27 @@ var pkg = JSON.parse(
|
|
|
56
58
|
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
57
59
|
);
|
|
58
60
|
var CLI_VERSION = pkg.version;
|
|
61
|
+
var CLI_PACKAGE = pkg.name;
|
|
62
|
+
|
|
63
|
+
// src/core/expert.ts
|
|
64
|
+
var EXPERT_TOKEN_PREFIX = "pxc_";
|
|
65
|
+
var EXPERT_ASSIGNMENTS_PATH = "/api/v1/expert/assignments";
|
|
66
|
+
function isExpertToken(token) {
|
|
67
|
+
return token.startsWith(EXPERT_TOKEN_PREFIX);
|
|
68
|
+
}
|
|
69
|
+
var EXPERT_REACH = "an outside expert's pxc_ credential only lists its own assignments and reads and ticks the gates it owns on them: assignments, conditions <issue> and gate satisfy|unsatisfy (assignment_list, condition_list and gate_set over MCP)";
|
|
70
|
+
var EXPERT_LAPSED = "the pxc_ credential is not valid here: it has expired (it lasts at most 30 days) or was revoked, its seat was revoked or has ended, the account is suspended, its TOTP enrolment changed, or it was minted for another host. If your seat is still open, mint a new one in your expert view";
|
|
71
|
+
var NOT_AN_EXPERT = "this credential is a member's login or an API token, not an outside expert's pxc_ credential";
|
|
72
|
+
function explainExpertRefusal(error, token) {
|
|
73
|
+
if (!(error instanceof HttpError) || error instanceof UpgradeRequiredError) return error;
|
|
74
|
+
const code = error.body["error"];
|
|
75
|
+
let hint;
|
|
76
|
+
if (error.status === 403 && code === "expert_bound_credential") hint = EXPERT_REACH;
|
|
77
|
+
else if (error.status === 403 && code === "expert_credential_required") hint = NOT_AN_EXPERT;
|
|
78
|
+
else if (error.status === 401 && isExpertToken(token)) hint = EXPERT_LAPSED;
|
|
79
|
+
if (hint === void 0) return error;
|
|
80
|
+
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
81
|
+
}
|
|
59
82
|
|
|
60
83
|
// src/core/client.ts
|
|
61
84
|
var RECEIVE_TIMEOUT_MS = 3e4;
|
|
@@ -89,10 +112,15 @@ var Client = class {
|
|
|
89
112
|
async get(path2, params) {
|
|
90
113
|
return this.json(await this.raw("GET", path2, { params }));
|
|
91
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* `actor: false` keeps a configured actor out of the body, for endpoints
|
|
117
|
+
* whose body is closed: they refuse a field they do not name, and record
|
|
118
|
+
* the credential's own person instead.
|
|
119
|
+
*/
|
|
92
120
|
async post(path2, body = {}, options = {}) {
|
|
93
121
|
return this.json(
|
|
94
122
|
await this.raw("POST", path2, {
|
|
95
|
-
json: this.withActor(body),
|
|
123
|
+
json: options.actor === false ? body : this.withActor(body),
|
|
96
124
|
idempotencyKey: options.idempotencyKey
|
|
97
125
|
})
|
|
98
126
|
);
|
|
@@ -122,16 +150,18 @@ var Client = class {
|
|
|
122
150
|
try {
|
|
123
151
|
return new Uint8Array(await response.arrayBuffer());
|
|
124
152
|
} catch (error) {
|
|
125
|
-
throw new TransportError(transportMessage(error));
|
|
153
|
+
throw new TransportError(transportMessage(error), transportCode(error));
|
|
126
154
|
}
|
|
127
155
|
}
|
|
128
156
|
/**
|
|
129
157
|
* The server's API version, for `version`'s server side. The header rides
|
|
130
158
|
* on every response — the versioning plug runs before auth — so even a
|
|
131
|
-
* 401 answers.
|
|
159
|
+
* 401 answers. A pxc_ credential probes the one route it reaches, so the
|
|
160
|
+
* probe is never a refusal in the server's log.
|
|
132
161
|
*/
|
|
133
162
|
async serverVersion() {
|
|
134
|
-
const
|
|
163
|
+
const path2 = isExpertToken(this.config.token) ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats";
|
|
164
|
+
const response = await this.raw("GET", path2, {});
|
|
135
165
|
return response.headers.get("x-harness-api-version") ?? "unknown";
|
|
136
166
|
}
|
|
137
167
|
// Only an explicitly configured actor is sent, and never over one the
|
|
@@ -168,7 +198,7 @@ var Client = class {
|
|
|
168
198
|
redirect: "manual"
|
|
169
199
|
});
|
|
170
200
|
} catch (error) {
|
|
171
|
-
throw new TransportError(transportMessage(error));
|
|
201
|
+
throw new TransportError(transportMessage(error), transportCode(error));
|
|
172
202
|
}
|
|
173
203
|
}
|
|
174
204
|
async json(response) {
|
|
@@ -177,7 +207,7 @@ var Client = class {
|
|
|
177
207
|
try {
|
|
178
208
|
text = await response.text();
|
|
179
209
|
} catch (error) {
|
|
180
|
-
throw new TransportError(transportMessage(error));
|
|
210
|
+
throw new TransportError(transportMessage(error), transportCode(error));
|
|
181
211
|
}
|
|
182
212
|
if (text === "") return { data: void 0 };
|
|
183
213
|
let parsed;
|
|
@@ -206,6 +236,17 @@ var Client = class {
|
|
|
206
236
|
return new HttpError(response.status, message, parsed, response.headers.get("retry-after"));
|
|
207
237
|
}
|
|
208
238
|
};
|
|
239
|
+
function transportCode(error) {
|
|
240
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
241
|
+
if (typeof cause !== "object" || cause === null) return null;
|
|
242
|
+
const code = cause.code;
|
|
243
|
+
if (typeof code === "string") return code;
|
|
244
|
+
const errors = cause.errors;
|
|
245
|
+
if (!Array.isArray(errors)) return null;
|
|
246
|
+
const codes = new Set(errors.map((inner) => inner?.code));
|
|
247
|
+
const [only] = codes;
|
|
248
|
+
return codes.size === 1 && typeof only === "string" ? only : null;
|
|
249
|
+
}
|
|
209
250
|
function transportMessage(error) {
|
|
210
251
|
if (error instanceof Error) {
|
|
211
252
|
if (error.name === "TimeoutError" || error.name === "AbortError") return "request timed out";
|
|
@@ -302,6 +343,10 @@ function activityEntry(entry) {
|
|
|
302
343
|
return activityStateEvent(entry);
|
|
303
344
|
case "assignee_event":
|
|
304
345
|
return activityAssigneeEvent(entry);
|
|
346
|
+
case "condition_event":
|
|
347
|
+
return activityConditionEvent(entry);
|
|
348
|
+
case "field_event":
|
|
349
|
+
return activityFieldEvent(entry);
|
|
305
350
|
default:
|
|
306
351
|
throw new CommandError(`Unknown activity entry kind: ${clean(entry["kind"])}`);
|
|
307
352
|
}
|
|
@@ -314,7 +359,7 @@ function activityComment(entry) {
|
|
|
314
359
|
if (typeof deleted !== "boolean") throw new CommandError("Invalid comment deleted flag.");
|
|
315
360
|
const body = activityCommentBody(comment2["body"], deleted);
|
|
316
361
|
const text = cleanMultiline(body).split("\n").map((line) => " " + line).join("\n");
|
|
317
|
-
return `[${
|
|
362
|
+
return `[${occurredAt(entry)}] ${clean(author)} (${clean(requiredString(comment2["id"], "comment id"))}):
|
|
318
363
|
${text}
|
|
319
364
|
`;
|
|
320
365
|
}
|
|
@@ -324,7 +369,7 @@ function activityCommentBody(value, deleted) {
|
|
|
324
369
|
return requiredString(value, "comment body");
|
|
325
370
|
}
|
|
326
371
|
function activityStateEvent(entry) {
|
|
327
|
-
const actor =
|
|
372
|
+
const actor = activityWho(entry);
|
|
328
373
|
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
329
374
|
let description;
|
|
330
375
|
switch (entry["action"]) {
|
|
@@ -340,11 +385,11 @@ function activityStateEvent(entry) {
|
|
|
340
385
|
default:
|
|
341
386
|
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
342
387
|
}
|
|
343
|
-
return `[${
|
|
388
|
+
return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
|
|
344
389
|
`;
|
|
345
390
|
}
|
|
346
391
|
function activityAssigneeEvent(entry) {
|
|
347
|
-
const actor =
|
|
392
|
+
const actor = activityWho(entry);
|
|
348
393
|
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
349
394
|
let description;
|
|
350
395
|
switch (entry["action"]) {
|
|
@@ -360,9 +405,130 @@ function activityAssigneeEvent(entry) {
|
|
|
360
405
|
default:
|
|
361
406
|
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
362
407
|
}
|
|
363
|
-
return `[${
|
|
408
|
+
return `[${occurredAt(entry)}] ${description} - ${provenance}
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
function activityConditionEvent(entry) {
|
|
412
|
+
const actor = activityWho(entry);
|
|
413
|
+
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
414
|
+
const condition = conditionSubject(requiredRecord(entry["condition"], "activity condition"));
|
|
415
|
+
let description;
|
|
416
|
+
switch (entry["action"]) {
|
|
417
|
+
case "added":
|
|
418
|
+
description = `${actor} added ${condition}`;
|
|
419
|
+
break;
|
|
420
|
+
case "changed":
|
|
421
|
+
description = `${actor} changed ${condition}`;
|
|
422
|
+
break;
|
|
423
|
+
case "removed":
|
|
424
|
+
description = `${actor} removed ${condition}`;
|
|
425
|
+
break;
|
|
426
|
+
case "satisfied":
|
|
427
|
+
description = `${actor} satisfied ${condition}`;
|
|
428
|
+
break;
|
|
429
|
+
case "unsatisfied":
|
|
430
|
+
description = `${actor} marked ${condition} unsatisfied`;
|
|
431
|
+
break;
|
|
432
|
+
default:
|
|
433
|
+
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
434
|
+
}
|
|
435
|
+
return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
|
|
364
436
|
`;
|
|
365
437
|
}
|
|
438
|
+
function conditionSubject(condition) {
|
|
439
|
+
const label = clean(condition["label"]);
|
|
440
|
+
switch (condition["kind"]) {
|
|
441
|
+
case "gate":
|
|
442
|
+
return label === "" ? "the gate" : `the gate "${label}"`;
|
|
443
|
+
case "not_before": {
|
|
444
|
+
const at = clean(condition["not_before"]);
|
|
445
|
+
return at === "" ? "the not-before date" : `the not-before date ${at}`;
|
|
446
|
+
}
|
|
447
|
+
case "event":
|
|
448
|
+
return label === "" ? "the wait condition" : `the system event "${label}"`;
|
|
449
|
+
default:
|
|
450
|
+
return "the wait condition";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function activityFieldEvent(entry) {
|
|
454
|
+
const actor = activityWho(entry);
|
|
455
|
+
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
456
|
+
const change = fieldChange(requiredString(entry["field"], "activity field"), entry["from"], entry["to"]);
|
|
457
|
+
return `[${occurredAt(entry)}] ${actor} ${change} \xB7 ${provenance}
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
function fieldChange(field3, from, to) {
|
|
461
|
+
switch (field3) {
|
|
462
|
+
case "priority":
|
|
463
|
+
return `changed priority from ${priorityName(from)} to ${priorityName(to)}`;
|
|
464
|
+
case "title":
|
|
465
|
+
return `renamed the issue from \u201C${activityTitle(from)}\u201D to \u201C${activityTitle(to)}\u201D`;
|
|
466
|
+
case "description":
|
|
467
|
+
return "edited the description";
|
|
468
|
+
case "labels":
|
|
469
|
+
return labelsChange(to);
|
|
470
|
+
case "parent":
|
|
471
|
+
return parentChange(from, to);
|
|
472
|
+
case "archived":
|
|
473
|
+
if (typeof to !== "boolean") throw new CommandError("Invalid activity archived value.");
|
|
474
|
+
return to ? "archived the issue" : "unarchived the issue";
|
|
475
|
+
default:
|
|
476
|
+
return `changed the ${clean(field3)} field`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
var ACTIVITY_PRIORITIES = {
|
|
480
|
+
0: "No priority",
|
|
481
|
+
1: "Urgent",
|
|
482
|
+
2: "High",
|
|
483
|
+
3: "Medium",
|
|
484
|
+
4: "Low"
|
|
485
|
+
};
|
|
486
|
+
function priorityName(value) {
|
|
487
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new CommandError("Invalid activity priority.");
|
|
488
|
+
return ACTIVITY_PRIORITIES[value] ?? String(value);
|
|
489
|
+
}
|
|
490
|
+
function activityTitle(value) {
|
|
491
|
+
if (typeof value !== "string") throw new CommandError("Invalid activity title.");
|
|
492
|
+
return clean(value);
|
|
493
|
+
}
|
|
494
|
+
function labelsChange(to) {
|
|
495
|
+
const change = requiredRecord(to, "activity labels");
|
|
496
|
+
const added = labelNames(change["added"]);
|
|
497
|
+
const removed = labelNames(change["removed"]);
|
|
498
|
+
const parts = [
|
|
499
|
+
...added.length > 0 ? [`added ${labelList(added)}`] : [],
|
|
500
|
+
...removed.length > 0 ? [`removed ${labelList(removed)}`] : []
|
|
501
|
+
];
|
|
502
|
+
return parts.length === 0 ? "changed the labels" : parts.join(" and ");
|
|
503
|
+
}
|
|
504
|
+
function labelNames(value) {
|
|
505
|
+
if (!Array.isArray(value)) throw new CommandError("Invalid activity labels.");
|
|
506
|
+
return value.map((label) => clean(requiredString(requiredRecord(label, "activity labels")["name"], "activity labels")));
|
|
507
|
+
}
|
|
508
|
+
function labelList(names) {
|
|
509
|
+
return `${names.length === 1 ? "label" : "labels"} ${names.join(", ")}`;
|
|
510
|
+
}
|
|
511
|
+
function parentChange(from, to) {
|
|
512
|
+
if (from === null && to === null) return "changed the parent";
|
|
513
|
+
if (from === null) return `set the parent to ${parentName(to)}`;
|
|
514
|
+
if (to === null) return `removed the parent ${parentName(from)}`;
|
|
515
|
+
return `changed the parent from ${parentName(from)} to ${parentName(to)}`;
|
|
516
|
+
}
|
|
517
|
+
function parentName(value) {
|
|
518
|
+
const parent = requiredRecord(value, "activity parent");
|
|
519
|
+
return optionalLabel(parent["identifier"], "an unknown issue", "activity parent identifier");
|
|
520
|
+
}
|
|
521
|
+
function activityWho(entry) {
|
|
522
|
+
const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
|
|
523
|
+
const label = entry["actor_label"];
|
|
524
|
+
if (label === void 0 || label === null || label === "") return actor;
|
|
525
|
+
if (typeof label !== "string") throw new CommandError("Invalid activity actor label.");
|
|
526
|
+
const cleaned = clean(label);
|
|
527
|
+
return cleaned === "" ? actor : `${actor} via \u201C${cleaned}\u201D`;
|
|
528
|
+
}
|
|
529
|
+
function occurredAt(entry) {
|
|
530
|
+
return clean(requiredString(entry["occurred_at"], "activity occurrence time"));
|
|
531
|
+
}
|
|
366
532
|
function assigneeName(value) {
|
|
367
533
|
const assignee = requiredRecord(value, "activity assignee");
|
|
368
534
|
if (assignee["kind"] === "unknown") return "Unknown assignee";
|
|
@@ -392,24 +558,24 @@ function activityProvenance(provenance) {
|
|
|
392
558
|
throw new CommandError(`Unknown activity provenance: ${clean(provenance["kind"])}`);
|
|
393
559
|
}
|
|
394
560
|
}
|
|
395
|
-
function stateName(value,
|
|
396
|
-
return optionalLabel(requiredRecord(value,
|
|
561
|
+
function stateName(value, field3) {
|
|
562
|
+
return optionalLabel(requiredRecord(value, field3)["name"], "Unknown state", `${field3} name`);
|
|
397
563
|
}
|
|
398
|
-
function nullableStateName(value,
|
|
399
|
-
return value === null ? "Unknown state" : stateName(value,
|
|
564
|
+
function nullableStateName(value, field3) {
|
|
565
|
+
return value === null ? "Unknown state" : stateName(value, field3);
|
|
400
566
|
}
|
|
401
|
-
function optionalLabel(value, fallback,
|
|
567
|
+
function optionalLabel(value, fallback, field3) {
|
|
402
568
|
if (value === null || value === "") return fallback;
|
|
403
|
-
return clean(requiredString(value,
|
|
569
|
+
return clean(requiredString(value, field3));
|
|
404
570
|
}
|
|
405
|
-
function requiredRecord(value,
|
|
571
|
+
function requiredRecord(value, field3) {
|
|
406
572
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
407
|
-
throw new CommandError(`Invalid ${
|
|
573
|
+
throw new CommandError(`Invalid ${field3}.`);
|
|
408
574
|
}
|
|
409
575
|
return value;
|
|
410
576
|
}
|
|
411
|
-
function requiredString(value,
|
|
412
|
-
if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${
|
|
577
|
+
function requiredString(value, field3) {
|
|
578
|
+
if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${field3}.`);
|
|
413
579
|
return value;
|
|
414
580
|
}
|
|
415
581
|
function stats(data) {
|
|
@@ -446,18 +612,25 @@ function waitingLine(issue) {
|
|
|
446
612
|
return `yes, on ${open} of ${conditions.length} wait condition${conditions.length === 1 ? "" : "s"}`;
|
|
447
613
|
}
|
|
448
614
|
function conditionsSection(conditions) {
|
|
449
|
-
const
|
|
450
|
-
if (
|
|
451
|
-
return "\nWait conditions:\n" +
|
|
615
|
+
const list3 = records(conditions);
|
|
616
|
+
if (list3.length === 0) return "";
|
|
617
|
+
return "\nWait conditions:\n" + list3.map((condition) => ` ${conditionLine(condition)}
|
|
452
618
|
`).join("");
|
|
453
619
|
}
|
|
454
|
-
function conditionLine(condition) {
|
|
620
|
+
function conditionLine(condition, options = {}) {
|
|
455
621
|
const satisfied = condition["satisfied"] === true;
|
|
456
622
|
let what;
|
|
457
623
|
switch (condition["kind"]) {
|
|
458
|
-
case "gate":
|
|
459
|
-
|
|
624
|
+
case "gate": {
|
|
625
|
+
const owner = condition["owner"];
|
|
626
|
+
let ownedBy = "";
|
|
627
|
+
if (typeof owner === "object" && owner !== null) {
|
|
628
|
+
const record = owner;
|
|
629
|
+
ownedBy = options.expertView === true && record["kind"] === "expert" ? "you" : ownerText(record);
|
|
630
|
+
}
|
|
631
|
+
what = `gate ${gateName(condition)}${ownedBy === "" ? "" : ` owned by ${ownedBy}`}`;
|
|
460
632
|
break;
|
|
633
|
+
}
|
|
461
634
|
case "not_before":
|
|
462
635
|
what = `not before ${clean(condition["not_before"])}`;
|
|
463
636
|
break;
|
|
@@ -469,11 +642,78 @@ function conditionLine(condition) {
|
|
|
469
642
|
}
|
|
470
643
|
let when = "";
|
|
471
644
|
if (satisfied && typeof condition["satisfied_at"] === "string") {
|
|
472
|
-
|
|
473
|
-
|
|
645
|
+
let by = clean(path(condition, "satisfied_by", "name"));
|
|
646
|
+
if (by === "" && condition["satisfied_by_kind"] === "expert") by = "an outside expert";
|
|
647
|
+
let role = typeof condition["satisfied_as_role"] === "string" ? roleName(condition["satisfied_as_role"]) : "";
|
|
648
|
+
switch (path(condition, "satisfied_by", "kind")) {
|
|
649
|
+
case "you":
|
|
650
|
+
by = "you";
|
|
651
|
+
break;
|
|
652
|
+
case "role": {
|
|
653
|
+
const as = path(condition, "satisfied_by", "role");
|
|
654
|
+
if (typeof as === "string" && as !== "") role = roleName(as);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case "expert":
|
|
658
|
+
by = "an outside expert";
|
|
659
|
+
break;
|
|
660
|
+
case "member":
|
|
661
|
+
by = "a member";
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
when = ` (satisfied ${clean(condition["satisfied_at"])}${by === "" ? "" : ` by ${by}`}${role === "" ? "" : ` as ${role}`})`;
|
|
474
665
|
}
|
|
475
666
|
return `${satisfied ? "[x]" : "[ ]"} ${what}${when} \xB7 ${clean(condition["id"])}`;
|
|
476
667
|
}
|
|
668
|
+
function gateName(gate) {
|
|
669
|
+
const key = clean(gate["key"]);
|
|
670
|
+
return `"${clean(gate["label"])}"${key === "" ? "" : ` [${key}]`}`;
|
|
671
|
+
}
|
|
672
|
+
function ownerText(owner) {
|
|
673
|
+
const label = clean(owner["label"]) || (owner["kind"] === "role" && typeof owner["role"] === "string" ? roleName(owner["role"]) : "");
|
|
674
|
+
const ref = clean(owner["ref"]);
|
|
675
|
+
if (owner["kind"] === "expert") {
|
|
676
|
+
const email = label || ref.replace(/^expert:/, "");
|
|
677
|
+
return email === "" ? "" : `${email} (expert)`;
|
|
678
|
+
}
|
|
679
|
+
if (label === "") return ref;
|
|
680
|
+
return ref === "" || ref === label ? label : `${label} (${ref})`;
|
|
681
|
+
}
|
|
682
|
+
function expertSummary(seat) {
|
|
683
|
+
const role = typeof seat.role === "string" && seat.role !== "" ? roleName(seat.role) : "Outside expert";
|
|
684
|
+
const status = clean(seat.plan?.status);
|
|
685
|
+
const plan = seat.plan === null ? ", no plan" : ` on plan ${clean(seat.plan.id)}${status === "" ? "" : ` (${status})`}`;
|
|
686
|
+
const steps = seat.assigned_steps;
|
|
687
|
+
const count = steps === 0 ? "no assigned steps" : `${steps} assigned step${steps === 1 ? "" : "s"}`;
|
|
688
|
+
return `${role}${plan} \xB7 ${count}`;
|
|
689
|
+
}
|
|
690
|
+
function expertAssignments(steps) {
|
|
691
|
+
return steps.map((step) => "\n" + assignedStep(step)).join("");
|
|
692
|
+
}
|
|
693
|
+
function assignedStep(step) {
|
|
694
|
+
const blockers = typeof step["open_blockers"] === "number" ? step["open_blockers"] : 0;
|
|
695
|
+
const fields = [
|
|
696
|
+
["State", path(step, "state", "name")],
|
|
697
|
+
["Due", step["due_date"]],
|
|
698
|
+
["Blocked", blockers > 0 ? `yes, by ${blockers} open issue${blockers === 1 ? "" : "s"}` : null]
|
|
699
|
+
];
|
|
700
|
+
const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
|
|
701
|
+
`).join("");
|
|
702
|
+
const gates = records(step["gates"]);
|
|
703
|
+
const gateLines = gates.length === 0 ? " Gates: none of yours\n" : " Gates:\n" + gates.map((gate) => ` ${conditionLine(gate, { expertView: true })}
|
|
704
|
+
`).join("");
|
|
705
|
+
const description = step["description"];
|
|
706
|
+
const descriptionLines = typeof description === "string" && description !== "" ? " Description:\n" + indent(indent(description)) + "\n" : "";
|
|
707
|
+
return `${clean(step["identifier"])} \xB7 ${clean(step["title"])}
|
|
708
|
+
${fieldLines}${gateLines}${descriptionLines}`;
|
|
709
|
+
}
|
|
710
|
+
var ROLE_NAMES = { gp: "GP" };
|
|
711
|
+
function roleName(role) {
|
|
712
|
+
const slug = clean(role);
|
|
713
|
+
if (Object.hasOwn(ROLE_NAMES, slug)) return ROLE_NAMES[slug];
|
|
714
|
+
const words = slug.replace(/_/g, " ");
|
|
715
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
716
|
+
}
|
|
477
717
|
var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
|
|
478
718
|
function planDetail(plan) {
|
|
479
719
|
const steps = records(plan["steps"]);
|
|
@@ -483,10 +723,10 @@ function planDetail(plan) {
|
|
|
483
723
|
["Budgets", planBudgetsLine(plan)],
|
|
484
724
|
["Estimate", planEstimateLine(plan["estimate"])]
|
|
485
725
|
];
|
|
486
|
-
const fieldLines = fields.filter((
|
|
726
|
+
const fieldLines = fields.filter((field3) => field3[1] !== null).map(([label, value]) => ` ${(label + ":").padEnd(12)}${value}
|
|
487
727
|
`).join("");
|
|
488
728
|
return `Plan ${clean(plan["id"])} \xB7 ${clean(plan["status"])}
|
|
489
|
-
` + fieldLines + planStepsSection(steps, str(plan["status"])) + planEdgesSection(plan["edges"]) + planConditionsSection(steps) + planHeldSection(plan["held_steps"]);
|
|
729
|
+
` + fieldLines + planStepsSection(steps, str(plan["status"])) + planEdgesSection(plan["edges"]) + planConditionsSection(steps) + planWaitingSection(steps) + planHeldSection(plan["held_steps"]);
|
|
490
730
|
}
|
|
491
731
|
function planRootLine(root) {
|
|
492
732
|
if (typeof root !== "object" || root === null) return null;
|
|
@@ -560,9 +800,9 @@ function stepStatus(step, planStatus) {
|
|
|
560
800
|
return "-";
|
|
561
801
|
}
|
|
562
802
|
function planEdgesSection(edges) {
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
return "\nEdges (blocker \u2192 blocked):\n" +
|
|
803
|
+
const list3 = records(edges);
|
|
804
|
+
if (list3.length === 0) return "";
|
|
805
|
+
return "\nEdges (blocker \u2192 blocked):\n" + list3.map((edge) => ` ${clean(edge["from"])} \u2192 ${clean(edge["to"])}
|
|
566
806
|
`).join("");
|
|
567
807
|
}
|
|
568
808
|
function planConditionsSection(steps) {
|
|
@@ -574,6 +814,32 @@ function planConditionsSection(steps) {
|
|
|
574
814
|
return "\nWait conditions:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
|
|
575
815
|
`).join("");
|
|
576
816
|
}
|
|
817
|
+
function planWaitingSection(steps) {
|
|
818
|
+
const lines = steps.flatMap(
|
|
819
|
+
(step) => records(step["waiting_for"]).map((wait) => [clean(step["identifier"]), waitLine(wait)])
|
|
820
|
+
);
|
|
821
|
+
if (lines.length === 0) return "";
|
|
822
|
+
const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
|
|
823
|
+
return "\nWaiting for:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
|
|
824
|
+
`).join("");
|
|
825
|
+
}
|
|
826
|
+
function waitLine(wait) {
|
|
827
|
+
const name = gateName(wait);
|
|
828
|
+
const owner = wait["owner"];
|
|
829
|
+
if (typeof owner !== "object" || owner === null) return `${name} \xB7 no owner, anyone with standing can tick`;
|
|
830
|
+
const holders = wait["holders"];
|
|
831
|
+
let who;
|
|
832
|
+
if (!Array.isArray(holders)) who = "who can tick is not known";
|
|
833
|
+
else if (holders.length === 0) who = "nobody can tick";
|
|
834
|
+
else who = `can tick: ${records(holders).map(holderText).join(", ")}`;
|
|
835
|
+
return `${name} \xB7 ${ownerText(owner)} \xB7 ${who}`;
|
|
836
|
+
}
|
|
837
|
+
function holderText(holder) {
|
|
838
|
+
const name = clean(holder["name"]);
|
|
839
|
+
if (holder["kind"] !== "expert") return name;
|
|
840
|
+
const organization = clean(holder["organization"]);
|
|
841
|
+
return `${name} (expert${organization === "" ? "" : `, ${organization}`})`;
|
|
842
|
+
}
|
|
577
843
|
function planHeldSection(heldSteps) {
|
|
578
844
|
if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
|
|
579
845
|
return `
|
|
@@ -588,10 +854,10 @@ function errorDetails(body) {
|
|
|
588
854
|
}
|
|
589
855
|
function changesetDetails(details) {
|
|
590
856
|
if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
|
|
591
|
-
return Object.entries(details).map(([
|
|
857
|
+
return Object.entries(details).map(([field3, messages]) => {
|
|
592
858
|
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
593
859
|
return `
|
|
594
|
-
${clean(
|
|
860
|
+
${clean(field3)}: ${clean(text)}`;
|
|
595
861
|
}).join("");
|
|
596
862
|
}
|
|
597
863
|
function violationDetails(violations) {
|
|
@@ -716,7 +982,7 @@ var SOURCES = [
|
|
|
716
982
|
var MAX_IMAGES_PER_SHOW = 20;
|
|
717
983
|
var MAX_ATTACHMENT_BYTES = 25e6;
|
|
718
984
|
function refs(issue) {
|
|
719
|
-
const texts = SOURCES.map(([
|
|
985
|
+
const texts = SOURCES.map(([field3, source]) => [issue[field3], source]);
|
|
720
986
|
const comments2 = Array.isArray(issue["comments"]) ? issue["comments"] : [];
|
|
721
987
|
for (const comment2 of comments2) texts.push([comment2["body"], "comment"]);
|
|
722
988
|
const found = [];
|
|
@@ -829,8 +1095,8 @@ function write(path2, bytes) {
|
|
|
829
1095
|
}
|
|
830
1096
|
|
|
831
1097
|
// src/core/pagination.ts
|
|
832
|
-
function notices(meta, requestedPageSize,
|
|
833
|
-
return capNotice(meta, requestedPageSize) + truncationNotice(meta,
|
|
1098
|
+
function notices(meta, requestedPageSize, records5) {
|
|
1099
|
+
return capNotice(meta, requestedPageSize) + truncationNotice(meta, records5);
|
|
834
1100
|
}
|
|
835
1101
|
function capNotice(meta, requestedPageSize) {
|
|
836
1102
|
const served = meta?.page_size;
|
|
@@ -840,12 +1106,12 @@ function capNotice(meta, requestedPageSize) {
|
|
|
840
1106
|
}
|
|
841
1107
|
return "";
|
|
842
1108
|
}
|
|
843
|
-
function truncationNotice(meta,
|
|
1109
|
+
function truncationNotice(meta, records5) {
|
|
844
1110
|
const total = meta?.total;
|
|
845
1111
|
const page = meta?.page;
|
|
846
1112
|
const pages = meta?.total_pages;
|
|
847
1113
|
if (typeof total !== "number" || typeof page !== "number" || typeof pages !== "number") return "";
|
|
848
|
-
const shown =
|
|
1114
|
+
const shown = records5.length;
|
|
849
1115
|
if (shown < total) {
|
|
850
1116
|
return `harness: showing ${shown} of ${total} (page ${page} of ${pages}) \u2014 pass --all for the rest, or --page to step through.
|
|
851
1117
|
`;
|
|
@@ -856,20 +1122,20 @@ async function fetchEveryPage(fetchPage) {
|
|
|
856
1122
|
const first = await fetchPage(1);
|
|
857
1123
|
const totalPages = pageCount(first.meta);
|
|
858
1124
|
if (totalPages === null) return first.data;
|
|
859
|
-
let
|
|
1125
|
+
let records5 = first.data;
|
|
860
1126
|
for (let page = 2; page <= totalPages; page++) {
|
|
861
1127
|
const body = await fetchPage(page);
|
|
862
|
-
|
|
1128
|
+
records5 = dedupe([...records5, ...body.data]);
|
|
863
1129
|
}
|
|
864
|
-
return
|
|
1130
|
+
return records5;
|
|
865
1131
|
}
|
|
866
1132
|
function pageCount(meta) {
|
|
867
1133
|
const total = meta?.total_pages;
|
|
868
1134
|
return typeof total === "number" && total > 0 ? total : null;
|
|
869
1135
|
}
|
|
870
|
-
function dedupe(
|
|
1136
|
+
function dedupe(records5) {
|
|
871
1137
|
const seen = /* @__PURE__ */ new Set();
|
|
872
|
-
return
|
|
1138
|
+
return records5.filter((record) => {
|
|
873
1139
|
const id = record["id"];
|
|
874
1140
|
const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
|
|
875
1141
|
if (seen.has(key)) return false;
|
|
@@ -908,9 +1174,9 @@ function requireIdentifiers(identifiers) {
|
|
|
908
1174
|
async function ready(ctx, opts) {
|
|
909
1175
|
if (opts.claim === true) return claimNext(ctx, opts);
|
|
910
1176
|
const params = { ...listParams(ctx, opts), ready: true };
|
|
911
|
-
const { records:
|
|
1177
|
+
const { records: records5, notice } = await fetchIssues(ctx, params, opts);
|
|
912
1178
|
const rank = (issue) => PRIORITY_RANK[issue["priority"]] ?? 4;
|
|
913
|
-
const sorted = markAgentSteps([...
|
|
1179
|
+
const sorted = markAgentSteps([...records5].sort((a, b) => rank(a) - rank(b)));
|
|
914
1180
|
return { json: sorted, human: issuesTable(sorted, { markAgentSteps: true }), notice };
|
|
915
1181
|
}
|
|
916
1182
|
function markAgentSteps(issues) {
|
|
@@ -965,13 +1231,13 @@ function isIssueRecord(value) {
|
|
|
965
1231
|
}
|
|
966
1232
|
async function blocked(ctx, opts) {
|
|
967
1233
|
const params = { ...listParams(ctx, opts), blocked: true };
|
|
968
|
-
const { records:
|
|
969
|
-
return { json:
|
|
1234
|
+
const { records: records5, notice } = await fetchIssues(ctx, params, opts);
|
|
1235
|
+
return { json: records5, human: issuesTable(records5), notice };
|
|
970
1236
|
}
|
|
971
1237
|
async function list(ctx, opts) {
|
|
972
1238
|
const params = listParams(ctx, opts);
|
|
973
|
-
const { records:
|
|
974
|
-
return { json:
|
|
1239
|
+
const { records: records5, notice } = await fetchIssues(ctx, params, opts);
|
|
1240
|
+
return { json: records5, human: issuesTable(records5), notice };
|
|
975
1241
|
}
|
|
976
1242
|
async function search(ctx, queryWords, opts) {
|
|
977
1243
|
if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
|
|
@@ -1002,9 +1268,14 @@ async function show(ctx, identifier, opts) {
|
|
|
1002
1268
|
return notice === "" ? { json: json2, human } : { json: json2, human, notice };
|
|
1003
1269
|
}
|
|
1004
1270
|
var ACTIVITY_MAX_LIMIT = 100;
|
|
1271
|
+
var ACTIVITY_INCLUDE = "condition_events,field_events";
|
|
1272
|
+
var ACTIVITY_INCLUDE_WITHOUT_FIELDS = "condition_events";
|
|
1273
|
+
var ACTIVITY_WITHOUT_FIELDS_NOTICE = "harness: this server doesn't record field changes yet; showing activity without them.\n";
|
|
1005
1274
|
async function activity(ctx, identifier, opts) {
|
|
1006
1275
|
requireIdentifiers([identifier]);
|
|
1007
|
-
|
|
1276
|
+
const first = await firstActivityPage(ctx, identifier, opts.limit);
|
|
1277
|
+
const include = first.include;
|
|
1278
|
+
let { page, notice } = first;
|
|
1008
1279
|
let entries = page.entries;
|
|
1009
1280
|
if (opts.all === true) {
|
|
1010
1281
|
const pages = [page.entries];
|
|
@@ -1015,17 +1286,38 @@ async function activity(ctx, identifier, opts) {
|
|
|
1015
1286
|
throw new CommandError("Activity response repeated a cursor.");
|
|
1016
1287
|
}
|
|
1017
1288
|
seenCursors.add(cursor);
|
|
1018
|
-
page = await activityPage(ctx, identifier, opts.limit, cursor);
|
|
1289
|
+
page = await activityPage(ctx, identifier, include, opts.limit, cursor);
|
|
1019
1290
|
pages.push(page.entries);
|
|
1020
1291
|
}
|
|
1021
1292
|
entries = pages.reverse().flat();
|
|
1022
1293
|
}
|
|
1023
|
-
|
|
1024
|
-
`
|
|
1025
|
-
|
|
1294
|
+
if (opts.all !== true && page.hasMore) {
|
|
1295
|
+
notice += `harness: showing the newest ${entries.length} activity entries \u2014 pass --all for older activity.
|
|
1296
|
+
`;
|
|
1297
|
+
}
|
|
1298
|
+
return { json: entries, human: activityEntries(entries), notice: notice === "" ? void 0 : notice };
|
|
1026
1299
|
}
|
|
1027
|
-
async function
|
|
1028
|
-
|
|
1300
|
+
async function firstActivityPage(ctx, identifier, limit) {
|
|
1301
|
+
try {
|
|
1302
|
+
const page2 = await activityPage(ctx, identifier, ACTIVITY_INCLUDE, limit);
|
|
1303
|
+
return { page: page2, include: ACTIVITY_INCLUDE, notice: "" };
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (!refusesInclude(error)) throw error;
|
|
1306
|
+
}
|
|
1307
|
+
const page = await activityPage(ctx, identifier, ACTIVITY_INCLUDE_WITHOUT_FIELDS, limit);
|
|
1308
|
+
return { page, include: ACTIVITY_INCLUDE_WITHOUT_FIELDS, notice: ACTIVITY_WITHOUT_FIELDS_NOTICE };
|
|
1309
|
+
}
|
|
1310
|
+
function refusesInclude(error) {
|
|
1311
|
+
if (!(error instanceof HttpError) || error.status !== 400) return false;
|
|
1312
|
+
const message = error.body["message"];
|
|
1313
|
+
return typeof message === "string" && /\binclude\b/.test(message) && /\b(condition_events|field_events)\b|\baccepts only\b/.test(message);
|
|
1314
|
+
}
|
|
1315
|
+
async function activityPage(ctx, identifier, include, limit, before) {
|
|
1316
|
+
const body = await ctx.client.get(`/api/v1/issues/${identifier}/activity`, {
|
|
1317
|
+
limit,
|
|
1318
|
+
before,
|
|
1319
|
+
include
|
|
1320
|
+
});
|
|
1029
1321
|
if (!Array.isArray(body.data)) throw new CommandError("Activity response data is not an array.");
|
|
1030
1322
|
if (body.data.some((entry) => typeof entry !== "object" || entry === null || Array.isArray(entry))) {
|
|
1031
1323
|
throw new CommandError("Activity response contains a non-object entry.");
|
|
@@ -1243,7 +1535,7 @@ async function close(ctx, identifiers, opts) {
|
|
|
1243
1535
|
results.push({ identifier, ok: true, response });
|
|
1244
1536
|
} catch (error) {
|
|
1245
1537
|
if (error instanceof HttpError || error instanceof TransportError) {
|
|
1246
|
-
results.push({ identifier, ok: false, error });
|
|
1538
|
+
results.push({ identifier, ok: false, error: explainExpertRefusal(error, ctx.config.token) });
|
|
1247
1539
|
} else {
|
|
1248
1540
|
throw error;
|
|
1249
1541
|
}
|
|
@@ -1293,7 +1585,7 @@ async function archive(ctx, identifiers, opts) {
|
|
|
1293
1585
|
results.push({ identifier, ok: true, issue: body.data });
|
|
1294
1586
|
} catch (error) {
|
|
1295
1587
|
if (error instanceof HttpError || error instanceof TransportError) {
|
|
1296
|
-
results.push({ identifier, ok: false, error });
|
|
1588
|
+
results.push({ identifier, ok: false, error: explainExpertRefusal(error, ctx.config.token) });
|
|
1297
1589
|
} else {
|
|
1298
1590
|
throw error;
|
|
1299
1591
|
}
|
|
@@ -1674,15 +1966,15 @@ async function fetchIssues(ctx, params, opts) {
|
|
|
1674
1966
|
throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
|
|
1675
1967
|
}
|
|
1676
1968
|
if (opts.all === true) {
|
|
1677
|
-
const
|
|
1969
|
+
const records6 = await fetchEveryPage(
|
|
1678
1970
|
(page) => ctx.client.get("/api/v1/issues", { ...params, page })
|
|
1679
1971
|
);
|
|
1680
|
-
return { records:
|
|
1972
|
+
return { records: records6, notice: "" };
|
|
1681
1973
|
}
|
|
1682
1974
|
const body = await ctx.client.get("/api/v1/issues", params);
|
|
1683
|
-
const
|
|
1975
|
+
const records5 = body.data;
|
|
1684
1976
|
const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
|
|
1685
|
-
return { records:
|
|
1977
|
+
return { records: records5, notice: notices(body.meta, requested, records5) };
|
|
1686
1978
|
}
|
|
1687
1979
|
function listParams(ctx, opts) {
|
|
1688
1980
|
if (opts.type !== void 0 && opts.label !== void 0) {
|
|
@@ -1755,8 +2047,8 @@ function withTeam(params, ctx, opts) {
|
|
|
1755
2047
|
function putParam(params, key, value) {
|
|
1756
2048
|
return value === void 0 ? params : { ...params, [key]: value };
|
|
1757
2049
|
}
|
|
1758
|
-
function putOpt(body,
|
|
1759
|
-
return value === void 0 ? body : { ...body, [
|
|
2050
|
+
function putOpt(body, field3, value) {
|
|
2051
|
+
return value === void 0 ? body : { ...body, [field3]: value };
|
|
1760
2052
|
}
|
|
1761
2053
|
function putPresent(body, key, value) {
|
|
1762
2054
|
if (value === null || value === void 0 || value === false) return body;
|
|
@@ -1808,8 +2100,8 @@ async function paginatedRead(ctx, path2, params, opts) {
|
|
|
1808
2100
|
throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
|
|
1809
2101
|
}
|
|
1810
2102
|
if (opts.all === true) {
|
|
1811
|
-
const
|
|
1812
|
-
return { records:
|
|
2103
|
+
const records5 = await fetchEveryPage((page) => ctx.client.get(path2, { ...params, page }));
|
|
2104
|
+
return { records: records5, notice: "" };
|
|
1813
2105
|
}
|
|
1814
2106
|
const body = await ctx.client.get(path2, params);
|
|
1815
2107
|
const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
|
|
@@ -1819,10 +2111,10 @@ async function projectList(ctx, opts) {
|
|
|
1819
2111
|
let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
|
|
1820
2112
|
if (opts.search !== void 0) params = { ...params, search: opts.search };
|
|
1821
2113
|
if (opts.state !== void 0) params = { ...params, state: opts.state };
|
|
1822
|
-
const { records:
|
|
1823
|
-
const human =
|
|
2114
|
+
const { records: records5, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
|
|
2115
|
+
const human = records5.length === 0 ? "No projects.\n" : table(
|
|
1824
2116
|
["ID", "STATE", "LEAD", "TARGET", "NAME"],
|
|
1825
|
-
|
|
2117
|
+
records5.map((project) => [
|
|
1826
2118
|
str3(project["id"]),
|
|
1827
2119
|
str3(project["state"]) || "-",
|
|
1828
2120
|
str3(field(project, "lead", "name")) || "-",
|
|
@@ -1830,7 +2122,7 @@ async function projectList(ctx, opts) {
|
|
|
1830
2122
|
str3(project["name"])
|
|
1831
2123
|
])
|
|
1832
2124
|
);
|
|
1833
|
-
return { json:
|
|
2125
|
+
return { json: records5, human, notice };
|
|
1834
2126
|
}
|
|
1835
2127
|
async function projectShow(ctx, id) {
|
|
1836
2128
|
requireSafeId("project", id);
|
|
@@ -1921,12 +2213,12 @@ async function milestoneCreate(ctx, opts) {
|
|
|
1921
2213
|
async function docList(ctx, opts) {
|
|
1922
2214
|
let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
|
|
1923
2215
|
if (opts.search !== void 0) params = { ...params, search: opts.search };
|
|
1924
|
-
const { records:
|
|
1925
|
-
const human =
|
|
2216
|
+
const { records: records5, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
|
|
2217
|
+
const human = records5.length === 0 ? "No documents.\n" : table(
|
|
1926
2218
|
["ID", "UPDATED", "TITLE"],
|
|
1927
|
-
|
|
2219
|
+
records5.map((doc) => [str3(doc["id"]), str3(doc["updated_at"]) || "-", str3(doc["title"])])
|
|
1928
2220
|
);
|
|
1929
|
-
return { json:
|
|
2221
|
+
return { json: records5, human, notice };
|
|
1930
2222
|
}
|
|
1931
2223
|
async function docShow(ctx, id) {
|
|
1932
2224
|
requireSafeId("document", id);
|
|
@@ -2012,7 +2304,7 @@ async function linkRm(ctx, issue, idOrUrl) {
|
|
|
2012
2304
|
return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
|
|
2013
2305
|
` };
|
|
2014
2306
|
}
|
|
2015
|
-
async function
|
|
2307
|
+
async function labelList2(ctx) {
|
|
2016
2308
|
const body = await ctx.client.get("/api/v1/labels");
|
|
2017
2309
|
const human = body.data.length === 0 ? "No labels.\n" : table(
|
|
2018
2310
|
["NAME", "COLOR", "ISSUES", "ID"],
|
|
@@ -2215,6 +2507,1112 @@ function indent2(text) {
|
|
|
2215
2507
|
return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
|
|
2216
2508
|
}
|
|
2217
2509
|
|
|
2510
|
+
// src/commands/operator-catalog.ts
|
|
2511
|
+
var SCHEMA_VIEWS = ["input", "result"];
|
|
2512
|
+
var MAX_QUERY_BYTES = 200;
|
|
2513
|
+
var MAX_CATALOG_PAGES = 200;
|
|
2514
|
+
var MAX_SCHEMA_PAGES = 1e3;
|
|
2515
|
+
var SCHEMA_ENCODING = "json_fragments_v1";
|
|
2516
|
+
var CAPABILITY_ID_FORMAT = /^[A-Za-z0-9_.:-]{1,200}$/;
|
|
2517
|
+
var APPROVAL_NOTE = "Write and send capabilities, and any proposal, need a person's approval in the app before they take effect.\n";
|
|
2518
|
+
var REFUSALS = {
|
|
2519
|
+
operator_disabled: "nothing here works until it is switched back on",
|
|
2520
|
+
operator_not_permitted: "this account has no admin or member role in the organization the credential is bound to",
|
|
2521
|
+
actor_required: "this credential acts as no person: sign in with `harness login`, or use an API token minted for a user",
|
|
2522
|
+
run_bound_credential: "a run-bound credential can't read Operator's catalog",
|
|
2523
|
+
forbidden: "Operator needs the operator:read scope (aone_ API tokens must be minted with it) and an admin or member role"
|
|
2524
|
+
};
|
|
2525
|
+
async function toolsList(ctx, opts) {
|
|
2526
|
+
requireToolsOpts(opts);
|
|
2527
|
+
const { items, meta } = await capabilityPages(ctx, {}, opts);
|
|
2528
|
+
return {
|
|
2529
|
+
json: items,
|
|
2530
|
+
human: items.length === 0 ? "Operator can use no capabilities here.\n" : capabilitiesTable(items, false),
|
|
2531
|
+
notice: pagingNotice(items, meta, opts, "capabilities")
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
async function toolsSearch(ctx, query, opts) {
|
|
2535
|
+
requireSearchQuery(query);
|
|
2536
|
+
requireToolsOpts(opts);
|
|
2537
|
+
const { items, meta } = await capabilityPages(ctx, { q: query }, opts);
|
|
2538
|
+
return {
|
|
2539
|
+
json: items,
|
|
2540
|
+
human: items.length === 0 ? `No capability Operator can use matches "${clean(query)}".
|
|
2541
|
+
` : capabilitiesTable(items, true),
|
|
2542
|
+
notice: pagingNotice(items, meta, opts, "matches")
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
async function toolsDomains(ctx) {
|
|
2546
|
+
const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/capabilities/domains"));
|
|
2547
|
+
const domains = records2(body.data, "domains");
|
|
2548
|
+
return {
|
|
2549
|
+
json: domains,
|
|
2550
|
+
human: domains.length === 0 ? "Operator can use no capabilities here.\n" : table(
|
|
2551
|
+
["DOMAIN", "CAPABILITIES"],
|
|
2552
|
+
domains.map((domain) => [str4(domain["domain"]), str4(domain["count"])])
|
|
2553
|
+
)
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
async function toolShow(ctx, id, opts) {
|
|
2557
|
+
requireCapabilityId(id);
|
|
2558
|
+
const view = requireSchemaView(opts.schema);
|
|
2559
|
+
const fragments = [];
|
|
2560
|
+
let capability = {};
|
|
2561
|
+
let fragmentCount = null;
|
|
2562
|
+
for (let page = 1; ; page++) {
|
|
2563
|
+
const body = await operatorRequest(
|
|
2564
|
+
() => ctx.client.get(`/api/v1/operator/capabilities/${id}`, { schema: view, page }),
|
|
2565
|
+
`Operator can't use a capability ${id} here: it doesn't exist, or it is disabled or switched off`
|
|
2566
|
+
);
|
|
2567
|
+
const data = body.data;
|
|
2568
|
+
if (!isRecord(data)) throw new CommandError("The capability response carried no capability.");
|
|
2569
|
+
if (data["id"] !== id || data["schema"] !== view || data["page"] !== page) {
|
|
2570
|
+
throw new CommandError(
|
|
2571
|
+
`The capability response is not page ${page} of ${id}'s ${view} schema (it says page ${clean(data["page"])} of ${clean(data["id"])}'s ${clean(data["schema"])} schema).`
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
if (data["schema_encoding"] !== SCHEMA_ENCODING) {
|
|
2575
|
+
throw new CommandError(
|
|
2576
|
+
`The capability's schema comes in an encoding this CLI does not read (${clean(data["schema_encoding"])}) \u2014 upgrade the CLI.`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
const count = data["fragment_count"];
|
|
2580
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 1) {
|
|
2581
|
+
throw new CommandError(`The capability's schema page ${page} carries no valid fragment count.`);
|
|
2582
|
+
}
|
|
2583
|
+
if (fragmentCount !== null && count !== fragmentCount) {
|
|
2584
|
+
throw new CommandError(
|
|
2585
|
+
`The capability's schema pages disagree on the fragment count (${fragmentCount}, then ${count} on page ${page}).`
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
fragmentCount = count;
|
|
2589
|
+
const pageFragments = data["fragments"];
|
|
2590
|
+
if (!Array.isArray(pageFragments) || !pageFragments.every((fragment) => typeof fragment === "string")) {
|
|
2591
|
+
throw new CommandError(`The capability's schema page ${page} carried no fragments.`);
|
|
2592
|
+
}
|
|
2593
|
+
fragments.push(...pageFragments);
|
|
2594
|
+
capability = data;
|
|
2595
|
+
const next = data["next_page"];
|
|
2596
|
+
if (next === null || next === void 0) break;
|
|
2597
|
+
if (next !== page + 1 || next > MAX_SCHEMA_PAGES) {
|
|
2598
|
+
throw new CommandError(`The capability's schema page ${page} names an invalid next page (${clean(next)}).`);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
if (fragments.length !== fragmentCount) {
|
|
2602
|
+
throw new CommandError(
|
|
2603
|
+
`The capability's schema came back incomplete: ${fragments.length} of ${fragmentCount} fragments.`
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
let schema;
|
|
2607
|
+
try {
|
|
2608
|
+
schema = JSON.parse(fragments.join(""));
|
|
2609
|
+
} catch {
|
|
2610
|
+
throw new CommandError("The capability's schema fragments do not join into valid JSON.");
|
|
2611
|
+
}
|
|
2612
|
+
const shown = { ...withoutSchemaPaging(capability), [`${view}_schema`]: schema };
|
|
2613
|
+
return { json: shown, human: capabilityDetail(shown, view, schema) };
|
|
2614
|
+
}
|
|
2615
|
+
async function skillsList(ctx) {
|
|
2616
|
+
const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/skills"));
|
|
2617
|
+
const skills = records2(body.data, "skills").map((skill) => ({
|
|
2618
|
+
id: skill["id"],
|
|
2619
|
+
title: skill["title"],
|
|
2620
|
+
invocation_name: skill["invocation_name"],
|
|
2621
|
+
content_sha256: skill["content_sha256"]
|
|
2622
|
+
}));
|
|
2623
|
+
return {
|
|
2624
|
+
json: skills,
|
|
2625
|
+
human: skills.length === 0 ? "No skills are assigned to Operator in this organization.\n" : table(
|
|
2626
|
+
["INVOCATION", "TITLE", "SHA-256", "ID"],
|
|
2627
|
+
skills.map((skill) => [
|
|
2628
|
+
str4(skill.invocation_name),
|
|
2629
|
+
truncate(skill.title),
|
|
2630
|
+
str4(skill.content_sha256),
|
|
2631
|
+
str4(skill.id)
|
|
2632
|
+
])
|
|
2633
|
+
)
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
function requireCapabilityId(id) {
|
|
2637
|
+
if (!CAPABILITY_ID_FORMAT.test(id) || /^\.+$/.test(id)) {
|
|
2638
|
+
throw new UsageError(`not a capability id: ${clean(id)} (e.g. crm.party.search)`);
|
|
2639
|
+
}
|
|
2640
|
+
if (id === "domains") {
|
|
2641
|
+
throw new UsageError("domains is not a capability id \u2014 `harness tools domains` lists the domains");
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
function requireSearchQuery(query) {
|
|
2645
|
+
if (query.trim() === "") throw new UsageError("the search query is empty \u2014 say what to search for");
|
|
2646
|
+
if (Buffer.byteLength(query, "utf8") > MAX_QUERY_BYTES) {
|
|
2647
|
+
throw new UsageError(`the search query is over ${MAX_QUERY_BYTES} bytes \u2014 shorten it`);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
function requireToolsOpts(opts) {
|
|
2651
|
+
if (opts.domain !== void 0 && opts.domain.trim() === "") {
|
|
2652
|
+
throw new UsageError("the domain is empty \u2014 name one, or leave it out for every domain");
|
|
2653
|
+
}
|
|
2654
|
+
if (opts.cursor !== void 0 && opts.cursor.trim() === "") {
|
|
2655
|
+
throw new UsageError("the cursor is empty \u2014 pass a page's cursor, or leave it out for the first page");
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
function requireSchemaView(view) {
|
|
2659
|
+
if (view === void 0) return "input";
|
|
2660
|
+
if (SCHEMA_VIEWS.includes(view)) return view;
|
|
2661
|
+
throw new UsageError(`--schema must be input or result, not ${clean(view)}`);
|
|
2662
|
+
}
|
|
2663
|
+
async function capabilityPages(ctx, base, opts) {
|
|
2664
|
+
const params = { ...base, domain: opts.domain };
|
|
2665
|
+
let page = await capabilityPage(ctx, { ...params, cursor: opts.cursor }, CALLER_CURSOR);
|
|
2666
|
+
const items = [...page.items];
|
|
2667
|
+
if (opts.all === true) {
|
|
2668
|
+
const seen = new Set(opts.cursor === void 0 ? [] : [opts.cursor]);
|
|
2669
|
+
let pages = 1;
|
|
2670
|
+
while (page.nextCursor !== null) {
|
|
2671
|
+
if (seen.has(page.nextCursor)) throw new CommandError("The capabilities response repeated a cursor.");
|
|
2672
|
+
if (pages >= MAX_CATALOG_PAGES) {
|
|
2673
|
+
throw new CommandError(`The capabilities listing ran past ${MAX_CATALOG_PAGES} pages; stopped there.`);
|
|
2674
|
+
}
|
|
2675
|
+
seen.add(page.nextCursor);
|
|
2676
|
+
page = await capabilityPage(ctx, { ...params, cursor: page.nextCursor }, WALK_CURSOR);
|
|
2677
|
+
pages++;
|
|
2678
|
+
items.push(...page.items);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return { items: dedupeById(items), meta: page.meta };
|
|
2682
|
+
}
|
|
2683
|
+
var CALLER_CURSOR = "start again without the cursor";
|
|
2684
|
+
var WALK_CURSOR = "the listing changed or its cursor expired while following pages; run it again";
|
|
2685
|
+
async function capabilityPage(ctx, params, staleCursor) {
|
|
2686
|
+
const body = await operatorRequest(
|
|
2687
|
+
() => ctx.client.get("/api/v1/operator/capabilities", params),
|
|
2688
|
+
void 0,
|
|
2689
|
+
staleCursor
|
|
2690
|
+
);
|
|
2691
|
+
const items = records2(body.data, "capabilities");
|
|
2692
|
+
const meta = body.meta;
|
|
2693
|
+
if (!isRecord(meta)) throw new CommandError("The capabilities response has no pagination metadata.");
|
|
2694
|
+
const nextCursor = meta.next_cursor;
|
|
2695
|
+
if (nextCursor !== null && (typeof nextCursor !== "string" || nextCursor === "")) {
|
|
2696
|
+
throw new CommandError("The capabilities response has an invalid next_cursor.");
|
|
2697
|
+
}
|
|
2698
|
+
if (nextCursor !== null && items.length === 0) {
|
|
2699
|
+
throw new CommandError("The capabilities response says there are more pages but holds no capabilities.");
|
|
2700
|
+
}
|
|
2701
|
+
return { items, meta, nextCursor };
|
|
2702
|
+
}
|
|
2703
|
+
async function operatorRequest(send, notFound2 = "this Harness server has no Operator API yet", staleCursor = CALLER_CURSOR) {
|
|
2704
|
+
try {
|
|
2705
|
+
return await send();
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
throw explain(error, notFound2, staleCursor);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
function explain(error, notFound2, staleCursor) {
|
|
2711
|
+
if (!(error instanceof HttpError)) return error;
|
|
2712
|
+
const code = typeof error.body["error"] === "string" ? error.body["error"] : "";
|
|
2713
|
+
let hint;
|
|
2714
|
+
if (error.status === 403) hint = Object.hasOwn(REFUSALS, code) ? REFUSALS[code] : void 0;
|
|
2715
|
+
else if (error.status === 404) hint = notFound2;
|
|
2716
|
+
else if (error.status === 400 && code === "invalid_cursor") hint = staleCursor;
|
|
2717
|
+
else if (error.status === 400 && /\bdomain\b/.test(error.message)) {
|
|
2718
|
+
hint = "no such domain; listing without one shows each capability's domain";
|
|
2719
|
+
}
|
|
2720
|
+
if (hint === void 0) return error;
|
|
2721
|
+
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
2722
|
+
}
|
|
2723
|
+
function capabilitiesTable(items, search2) {
|
|
2724
|
+
const headers = ["ID", "KIND", "DOMAIN", "SUMMARY"];
|
|
2725
|
+
const rows = items.map((item) => [
|
|
2726
|
+
str4(item["id"]),
|
|
2727
|
+
kindName(item),
|
|
2728
|
+
str4(item["domain"]),
|
|
2729
|
+
truncate(item["summary"])
|
|
2730
|
+
]);
|
|
2731
|
+
const table2 = search2 ? table(
|
|
2732
|
+
["SCORE", ...headers],
|
|
2733
|
+
rows.map((row, index) => [str4(items[index]["score"]), ...row])
|
|
2734
|
+
) : table(headers, rows);
|
|
2735
|
+
return table2 + (items.some(needsApproval) ? "\n" + APPROVAL_NOTE : "");
|
|
2736
|
+
}
|
|
2737
|
+
function pagingNotice(items, meta, opts, noun) {
|
|
2738
|
+
if (opts.all === true || typeof meta.next_cursor !== "string") return void 0;
|
|
2739
|
+
const of = typeof meta.count === "number" ? ` of ${meta.count}` : "";
|
|
2740
|
+
return `harness: showing ${items.length}${of} ${noun} \u2014 pass --all for every page, or --cursor ${clean(meta.next_cursor)} for the next one.
|
|
2741
|
+
`;
|
|
2742
|
+
}
|
|
2743
|
+
function capabilityDetail(capability, view, schema) {
|
|
2744
|
+
const kind = kindName(capability);
|
|
2745
|
+
const line = (label, value) => ` ${(label + ":").padEnd(10)}${clean(value)}
|
|
2746
|
+
`;
|
|
2747
|
+
const description = cleanMultiline(str4(capability["description"]));
|
|
2748
|
+
return `${clean(capability["id"])}
|
|
2749
|
+
` + line("Domain", str4(capability["domain"])) + line("Kind", kind === "send" ? "send \u2014 delivered beyond the platform" : kind) + line("Mode", `${str4(capability["mode"])} (effect: ${str4(capability["effect"])})`) + line("Summary", str4(capability["summary"])) + line("Approval", approval(capability)) + (description === "" ? "" : `
|
|
2750
|
+
Description:
|
|
2751
|
+
${indent3(description)}
|
|
2752
|
+
`) + `
|
|
2753
|
+
${view === "input" ? "Input" : "Result"} schema:
|
|
2754
|
+
${indent3(cleanMultiline(JSON.stringify(schema, null, 2)))}
|
|
2755
|
+
`;
|
|
2756
|
+
}
|
|
2757
|
+
function needsApproval(capability) {
|
|
2758
|
+
return capability["kind"] === "write" || capability["kind"] === "send" || capability["mode"] === "proposal";
|
|
2759
|
+
}
|
|
2760
|
+
function approval(capability) {
|
|
2761
|
+
if (needsApproval(capability)) return "a person approves it in the app before it takes effect";
|
|
2762
|
+
if (capability["kind"] === "read") return "not needed for a read";
|
|
2763
|
+
const kind = kindName(capability);
|
|
2764
|
+
return kind === "unknown" ? "unknown: the server named no kind" : `unknown for a capability of kind ${kind}`;
|
|
2765
|
+
}
|
|
2766
|
+
function kindName(capability) {
|
|
2767
|
+
const kind = capability["kind"];
|
|
2768
|
+
return typeof kind === "string" && kind.trim() !== "" ? kind : "unknown";
|
|
2769
|
+
}
|
|
2770
|
+
var SCHEMA_PAGING_FIELDS = /* @__PURE__ */ new Set(["schema", "schema_encoding", "fragments", "fragment_count", "page", "next_page"]);
|
|
2771
|
+
function withoutSchemaPaging(capability) {
|
|
2772
|
+
return Object.fromEntries(Object.entries(capability).filter(([key]) => !SCHEMA_PAGING_FIELDS.has(key)));
|
|
2773
|
+
}
|
|
2774
|
+
function records2(value, what) {
|
|
2775
|
+
if (!Array.isArray(value) || !value.every(isRecord)) {
|
|
2776
|
+
throw new CommandError(`The ${what} response carried no list.`);
|
|
2777
|
+
}
|
|
2778
|
+
return value;
|
|
2779
|
+
}
|
|
2780
|
+
function dedupeById(items) {
|
|
2781
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2782
|
+
return items.filter((item) => {
|
|
2783
|
+
if (seen.has(item["id"])) return false;
|
|
2784
|
+
seen.add(item["id"]);
|
|
2785
|
+
return true;
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
function indent3(text) {
|
|
2789
|
+
return text.split("\n").map((line) => " " + line).join("\n");
|
|
2790
|
+
}
|
|
2791
|
+
function isRecord(value) {
|
|
2792
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2793
|
+
}
|
|
2794
|
+
function str4(value) {
|
|
2795
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// src/commands/operator-jobs.ts
|
|
2799
|
+
var JOB_STATES = [
|
|
2800
|
+
"draft",
|
|
2801
|
+
"planning",
|
|
2802
|
+
"ready",
|
|
2803
|
+
"running",
|
|
2804
|
+
"waiting_for_approval",
|
|
2805
|
+
"waiting_for_human",
|
|
2806
|
+
"paused",
|
|
2807
|
+
"failed",
|
|
2808
|
+
"needs_reconciliation",
|
|
2809
|
+
"completed",
|
|
2810
|
+
"cancelled"
|
|
2811
|
+
];
|
|
2812
|
+
var JOB_STATUS_ALIASES = ["open", "waiting", "done"];
|
|
2813
|
+
var JOBS_MAX_LIMIT = 50;
|
|
2814
|
+
var JOBS_MAX_PAGES = 100;
|
|
2815
|
+
var EVENTS_PAGE_LIMIT = 200;
|
|
2816
|
+
var WATCH_INTERVAL_MS = 2e3;
|
|
2817
|
+
var WATCH_WAITING_INTERVAL_MS = 1e4;
|
|
2818
|
+
var WATCH_RETRIES = 3;
|
|
2819
|
+
var RETRY_BASE_MS = 1e3;
|
|
2820
|
+
var RETRY_AFTER_CAP_MS = 3e5;
|
|
2821
|
+
var EXIT_WAITING_FOR_PERSON = 4;
|
|
2822
|
+
var EXIT_JOB_ENDED = 5;
|
|
2823
|
+
var EXIT_INTERRUPTED = 130;
|
|
2824
|
+
var INTERRUPTED = "interrupted";
|
|
2825
|
+
var READER_GONE = "reader-gone";
|
|
2826
|
+
var ENDED_STATES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2827
|
+
var CHANGE_SET_FIELDS = [
|
|
2828
|
+
"manifest_id",
|
|
2829
|
+
"version",
|
|
2830
|
+
"digest",
|
|
2831
|
+
"status",
|
|
2832
|
+
"plan_version",
|
|
2833
|
+
"max_effect_count",
|
|
2834
|
+
"actions"
|
|
2835
|
+
];
|
|
2836
|
+
var CHANGE_SET_ACTION_FIELDS = [
|
|
2837
|
+
"id",
|
|
2838
|
+
"action_type",
|
|
2839
|
+
"action_version",
|
|
2840
|
+
"display_order",
|
|
2841
|
+
"effect_count",
|
|
2842
|
+
"required_capability_ids",
|
|
2843
|
+
"digest"
|
|
2844
|
+
];
|
|
2845
|
+
var WITHHELD_FIELDS = /* @__PURE__ */ new Set([
|
|
2846
|
+
"objective",
|
|
2847
|
+
"objective_text",
|
|
2848
|
+
"instruction",
|
|
2849
|
+
"instructions",
|
|
2850
|
+
"description",
|
|
2851
|
+
"result",
|
|
2852
|
+
"results",
|
|
2853
|
+
"outputs",
|
|
2854
|
+
"investigation",
|
|
2855
|
+
"read_material",
|
|
2856
|
+
"public_research",
|
|
2857
|
+
"source_review",
|
|
2858
|
+
"review_actions",
|
|
2859
|
+
"manifest_actions",
|
|
2860
|
+
"recipe",
|
|
2861
|
+
"lua",
|
|
2862
|
+
"lua_source"
|
|
2863
|
+
]);
|
|
2864
|
+
var CODE = /^[a-z0-9_.:-]{1,64}$/;
|
|
2865
|
+
var TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
2866
|
+
function requireJobsArgs(opts) {
|
|
2867
|
+
if (opts.status !== void 0) normalizeStatus(opts.status);
|
|
2868
|
+
if (opts.limit !== void 0 && (!Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > JOBS_MAX_LIMIT)) {
|
|
2869
|
+
throw new UsageError(`--limit takes an integer from 1 to ${JOBS_MAX_LIMIT}`);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
function normalizeStatus(status) {
|
|
2873
|
+
const tokens = status.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "");
|
|
2874
|
+
const known = [...JOB_STATES, ...JOB_STATUS_ALIASES];
|
|
2875
|
+
const unknown = tokens.filter((token) => !known.includes(token));
|
|
2876
|
+
if (tokens.length === 0 || unknown.length > 0) {
|
|
2877
|
+
throw new UsageError(
|
|
2878
|
+
`${tokens.length === 0 ? "--status is empty" : `unknown job status: ${unknown.join(", ")}`} \u2014 pass job states (${JOB_STATES.join(", ")}) or ${JOB_STATUS_ALIASES.join(", ")}, comma-separated`
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
return tokens.join(",");
|
|
2882
|
+
}
|
|
2883
|
+
async function jobsList(ctx, opts) {
|
|
2884
|
+
requireJobsArgs(opts);
|
|
2885
|
+
const params = {
|
|
2886
|
+
status: opts.status === void 0 ? void 0 : normalizeStatus(opts.status),
|
|
2887
|
+
mine: opts.mine === true ? true : void 0,
|
|
2888
|
+
// Reading everything: full pages, so the walk takes as few requests as it can.
|
|
2889
|
+
limit: opts.limit ?? (opts.all === true ? JOBS_MAX_LIMIT : void 0)
|
|
2890
|
+
};
|
|
2891
|
+
let page = await jobsPage(ctx, params, void 0);
|
|
2892
|
+
let jobs = page.jobs;
|
|
2893
|
+
if (opts.all !== true) {
|
|
2894
|
+
if (!page.hasMore) return { json: jobs, human: jobsTable(jobs) };
|
|
2895
|
+
if (jobs.length === 0) {
|
|
2896
|
+
return {
|
|
2897
|
+
json: jobs,
|
|
2898
|
+
human: "No Operator jobs you can see on this page.\n",
|
|
2899
|
+
notice: "harness: older jobs may exist past this page \u2014 pass --all to read on.\n"
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
json: jobs,
|
|
2904
|
+
human: jobsTable(jobs),
|
|
2905
|
+
notice: `harness: showing the newest ${plural(jobs.length, "Operator job")} \u2014 pass --all for older ones.
|
|
2906
|
+
`
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2910
|
+
let pages = 1;
|
|
2911
|
+
while (page.hasMore && pages < JOBS_MAX_PAGES) {
|
|
2912
|
+
const cursor = page.nextCursor;
|
|
2913
|
+
if (seen.has(cursor)) throw new CommandError("The jobs response repeated a cursor.");
|
|
2914
|
+
seen.add(cursor);
|
|
2915
|
+
page = await jobsPage(ctx, params, cursor);
|
|
2916
|
+
pages += 1;
|
|
2917
|
+
jobs = [...jobs, ...page.jobs];
|
|
2918
|
+
}
|
|
2919
|
+
jobs = uniqueById(jobs);
|
|
2920
|
+
if (!page.hasMore) return { json: jobs, human: jobsTable(jobs) };
|
|
2921
|
+
return {
|
|
2922
|
+
json: jobs,
|
|
2923
|
+
human: jobsTable(jobs),
|
|
2924
|
+
notice: `harness: stopped after ${JOBS_MAX_PAGES} pages (${plural(jobs.length, "job")}) without reaching the end \u2014 narrow the list with --status or --mine.
|
|
2925
|
+
`,
|
|
2926
|
+
exitCode: 1
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
async function jobsPage(ctx, params, before) {
|
|
2930
|
+
const body = await explained(
|
|
2931
|
+
() => ctx.client.get("/api/v1/operator/jobs", { ...params, before }),
|
|
2932
|
+
{ notFound: "this Harness server has no Operator API yet" }
|
|
2933
|
+
);
|
|
2934
|
+
const jobs = objects(body.data, "jobs").map(withheld);
|
|
2935
|
+
const meta = body.meta;
|
|
2936
|
+
if (typeof meta !== "object" || meta === null || typeof meta["has_more"] !== "boolean") {
|
|
2937
|
+
throw new CommandError("The jobs response has no paging metadata.");
|
|
2938
|
+
}
|
|
2939
|
+
if (meta["has_more"] !== true) return { jobs, hasMore: false, nextCursor: null };
|
|
2940
|
+
const cursor = meta["next_cursor"];
|
|
2941
|
+
if (typeof cursor !== "string" || cursor === "") {
|
|
2942
|
+
throw new CommandError("The jobs response says more exist but gives no cursor.");
|
|
2943
|
+
}
|
|
2944
|
+
return { jobs, hasMore: true, nextCursor: cursor };
|
|
2945
|
+
}
|
|
2946
|
+
function requireJobId(id) {
|
|
2947
|
+
if (!isUuid(id)) throw new UsageError(`not an Operator job id: ${id} (expected the job's UUID)`);
|
|
2948
|
+
}
|
|
2949
|
+
async function jobShow(ctx, id, opts = {}) {
|
|
2950
|
+
requireJobId(id);
|
|
2951
|
+
const body = await explained(() => ctx.client.get(`/api/v1/operator/jobs/${id}`), {
|
|
2952
|
+
notFound: missingJob(id)
|
|
2953
|
+
});
|
|
2954
|
+
if (!isRecord2(body.data)) throw new CommandError("The job response carried no job.");
|
|
2955
|
+
const job = withheld(body.data);
|
|
2956
|
+
if (opts.changes !== true && opts.people !== true) return { json: job, human: jobDetail(job) };
|
|
2957
|
+
const document = Object.fromEntries(
|
|
2958
|
+
["id", "state", "waiting_on", "url", "approval_url"].map((key) => [key, job[key] ?? null])
|
|
2959
|
+
);
|
|
2960
|
+
const sections = [];
|
|
2961
|
+
let notice;
|
|
2962
|
+
if (opts.changes === true) {
|
|
2963
|
+
const changeSet = await jobChangeSet(ctx, id);
|
|
2964
|
+
document["change_set"] = changeSet;
|
|
2965
|
+
sections.push(changesText(id, job, changeSet));
|
|
2966
|
+
notice = changeSetRace(job, changeSet);
|
|
2967
|
+
}
|
|
2968
|
+
if (opts.people === true) {
|
|
2969
|
+
const people = jobPeople(job);
|
|
2970
|
+
document["people"] = people;
|
|
2971
|
+
sections.push(peopleText(id, people));
|
|
2972
|
+
}
|
|
2973
|
+
return { json: document, human: sections.join("\n"), ...notice === void 0 ? {} : { notice } };
|
|
2974
|
+
}
|
|
2975
|
+
function changeSetRace(job, changeSet) {
|
|
2976
|
+
const pending = field2(job, "pending_approval", "manifest_id");
|
|
2977
|
+
if (typeof pending !== "string") return void 0;
|
|
2978
|
+
const read = changeSet === null ? null : changeSet["manifest_id"];
|
|
2979
|
+
if (read === pending) return void 0;
|
|
2980
|
+
return `harness: the job waits for approval of change set ${clean(pending)}, but the change set read next is ${read === null ? "none" : clean(read)}: the job changed between the two reads, so run it again.
|
|
2981
|
+
`;
|
|
2982
|
+
}
|
|
2983
|
+
async function jobChangeSet(ctx, id) {
|
|
2984
|
+
const body = await explained(() => ctx.client.get(`/api/v1/operator/jobs/${id}/changes`), {
|
|
2985
|
+
notFound: missingJob(id)
|
|
2986
|
+
});
|
|
2987
|
+
if (body.data === null) return null;
|
|
2988
|
+
if (!isRecord2(body.data)) throw new CommandError("The change set response carried no change set.");
|
|
2989
|
+
return {
|
|
2990
|
+
...pick(body.data, CHANGE_SET_FIELDS),
|
|
2991
|
+
actions: records3(body.data["actions"]).map((action) => pick(action, CHANGE_SET_ACTION_FIELDS))
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
function jobPeople(job) {
|
|
2995
|
+
const participants = records3(field2(job, "people", "participants"));
|
|
2996
|
+
const owner = isRecord2(job["owner"]) ? job["owner"] : null;
|
|
2997
|
+
const names = /* @__PURE__ */ new Map();
|
|
2998
|
+
for (const named of owner === null ? participants : [...participants, owner]) {
|
|
2999
|
+
if (typeof named["user_id"] === "string" && typeof named["name"] === "string" && named["name"] !== "") {
|
|
3000
|
+
if (!names.has(named["user_id"])) names.set(named["user_id"], named["name"]);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
const person = (userId) => typeof userId === "string" && userId !== "" ? { user_id: userId, name: names.get(userId) ?? null } : null;
|
|
3004
|
+
const some = (people) => people.filter((entry) => entry !== null);
|
|
3005
|
+
const approvers = field2(job, "people", "approver_user_ids");
|
|
3006
|
+
return {
|
|
3007
|
+
requester: person(job["requester_user_id"]),
|
|
3008
|
+
execution_principal: person(job["execution_principal_user_id"]),
|
|
3009
|
+
owner: owner === null ? null : person(owner["user_id"]),
|
|
3010
|
+
approvers: some((Array.isArray(approvers) ? approvers : []).map(person)),
|
|
3011
|
+
collaborators: some(participants.filter((entry) => entry["role"] === "collaborator").map((entry) => person(entry["user_id"])))
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
async function jobWatch(ctx, id, opts) {
|
|
3015
|
+
requireJobId(id);
|
|
3016
|
+
if (opts.after !== void 0 && (!Number.isInteger(opts.after) || opts.after < 0)) {
|
|
3017
|
+
throw new UsageError("--after takes a non-negative event sequence");
|
|
3018
|
+
}
|
|
3019
|
+
const emit = opts.onLine ?? (() => {
|
|
3020
|
+
});
|
|
3021
|
+
const sleep = opts.sleep ?? abortableSleep;
|
|
3022
|
+
const signal = opts.signal;
|
|
3023
|
+
let after = opts.after ?? 0;
|
|
3024
|
+
const events = [];
|
|
3025
|
+
let job = null;
|
|
3026
|
+
let shown = null;
|
|
3027
|
+
let shownApprovalUrl = null;
|
|
3028
|
+
const document = (outcome, extra = {}) => ({
|
|
3029
|
+
id,
|
|
3030
|
+
outcome,
|
|
3031
|
+
...extra,
|
|
3032
|
+
job: job === null ? null : jobStateJson(job),
|
|
3033
|
+
...opts.collectEvents === true ? { events } : {},
|
|
3034
|
+
next_after: after
|
|
3035
|
+
});
|
|
3036
|
+
const stopped = (reason) => reason === READER_GONE ? (
|
|
3037
|
+
// Nobody is reading any more: end quietly.
|
|
3038
|
+
{ json: document("stopped"), human: "", exitCode: 0 }
|
|
3039
|
+
) : { json: document("interrupted"), human: "", notice: resumeLine(id, after), exitCode: EXIT_INTERRUPTED };
|
|
3040
|
+
for (; ; ) {
|
|
3041
|
+
if (aborted(signal)) return stopped(signal?.reason);
|
|
3042
|
+
let page;
|
|
3043
|
+
try {
|
|
3044
|
+
page = await withRetries(() => eventsPage(ctx, id, after), sleep, signal);
|
|
3045
|
+
} catch (error) {
|
|
3046
|
+
if (aborted(signal)) return stopped(signal?.reason);
|
|
3047
|
+
if (!transient(error)) throw error;
|
|
3048
|
+
const message = failureMessage(error);
|
|
3049
|
+
return {
|
|
3050
|
+
json: document("error", {
|
|
3051
|
+
error: { message, status: error instanceof HttpError ? error.status : null }
|
|
3052
|
+
}),
|
|
3053
|
+
human: "",
|
|
3054
|
+
notice: `harness: ${message}
|
|
3055
|
+
` + resumeLine(id, after),
|
|
3056
|
+
exitCode: error instanceof TransportError ? 3 : 1
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
for (const event of page.events) {
|
|
3060
|
+
if (opts.collectEvents === true) events.push(eventJson(event));
|
|
3061
|
+
emit(eventLine(event));
|
|
3062
|
+
}
|
|
3063
|
+
after = page.nextAfter;
|
|
3064
|
+
job = page.job;
|
|
3065
|
+
if (page.hasMore) continue;
|
|
3066
|
+
const state = str5(job["state"]);
|
|
3067
|
+
const waitingOn = typeof job["waiting_on"] === "string" ? job["waiting_on"] : null;
|
|
3068
|
+
if (shown === null || shown.state !== state || shown.waitingOn !== waitingOn) {
|
|
3069
|
+
emit(stateLine(shown?.state ?? null, state, waitingOn));
|
|
3070
|
+
shown = { state, waitingOn };
|
|
3071
|
+
}
|
|
3072
|
+
const approvalUrl = typeof job["approval_url"] === "string" ? job["approval_url"] : null;
|
|
3073
|
+
if (opts.throughWaits === true && approvalUrl !== null && approvalUrl !== shownApprovalUrl) {
|
|
3074
|
+
emit(`approve in the app: ${clean(approvalUrl)}
|
|
3075
|
+
`);
|
|
3076
|
+
}
|
|
3077
|
+
shownApprovalUrl = approvalUrl;
|
|
3078
|
+
let outcome = null;
|
|
3079
|
+
if (ENDED_STATES.has(state)) outcome = state;
|
|
3080
|
+
else if (waitingOn !== null && opts.throughWaits !== true) outcome = "waiting";
|
|
3081
|
+
if (outcome !== null) {
|
|
3082
|
+
return {
|
|
3083
|
+
json: document(outcome),
|
|
3084
|
+
human: outcomeText(id, outcome, job),
|
|
3085
|
+
exitCode: outcome === "completed" ? 0 : outcome === "waiting" ? EXIT_WAITING_FOR_PERSON : EXIT_JOB_ENDED
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
if (aborted(signal)) return stopped(signal?.reason);
|
|
3089
|
+
await sleep(waitingOn !== null || state === "paused" ? WATCH_WAITING_INTERVAL_MS : WATCH_INTERVAL_MS, signal);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
function resumeLine(id, after) {
|
|
3093
|
+
return `harness: stopped watching job ${id}; resume with: harness watch ${id} --after ${after}
|
|
3094
|
+
`;
|
|
3095
|
+
}
|
|
3096
|
+
function watchStopSignal(stdout = process.stdout, signals = process) {
|
|
3097
|
+
const controller = new AbortController();
|
|
3098
|
+
const readerGone = () => controller.abort(READER_GONE);
|
|
3099
|
+
stdout.once("error", readerGone);
|
|
3100
|
+
stdout.once("close", readerGone);
|
|
3101
|
+
signals.once("SIGINT", () => controller.abort(INTERRUPTED));
|
|
3102
|
+
return controller.signal;
|
|
3103
|
+
}
|
|
3104
|
+
function aborted(signal) {
|
|
3105
|
+
return signal?.aborted === true;
|
|
3106
|
+
}
|
|
3107
|
+
function abortableSleep(ms, signal) {
|
|
3108
|
+
return new Promise((resolve) => {
|
|
3109
|
+
if (aborted(signal)) {
|
|
3110
|
+
resolve();
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
const done = () => {
|
|
3114
|
+
clearTimeout(timer);
|
|
3115
|
+
signal?.removeEventListener("abort", done);
|
|
3116
|
+
resolve();
|
|
3117
|
+
};
|
|
3118
|
+
const timer = setTimeout(done, ms);
|
|
3119
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
function transient(error) {
|
|
3123
|
+
if (error instanceof TransportError) return true;
|
|
3124
|
+
return error instanceof HttpError && !(error instanceof UpgradeRequiredError) && (error.status >= 500 || error.status === 429);
|
|
3125
|
+
}
|
|
3126
|
+
async function withRetries(send, sleep, signal) {
|
|
3127
|
+
for (let attempt = 0; ; attempt++) {
|
|
3128
|
+
try {
|
|
3129
|
+
return await send();
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
if (attempt >= WATCH_RETRIES || !transient(error) || aborted(signal)) throw error;
|
|
3132
|
+
await sleep(retryDelay(error, attempt), signal);
|
|
3133
|
+
if (aborted(signal)) throw error;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function retryDelay(error, attempt) {
|
|
3138
|
+
const backoff = RETRY_BASE_MS * 2 ** attempt;
|
|
3139
|
+
const asked = error instanceof HttpError ? retryAfterMs(error.retryAfter) : null;
|
|
3140
|
+
return asked === null ? backoff : Math.max(backoff, Math.min(asked, RETRY_AFTER_CAP_MS));
|
|
3141
|
+
}
|
|
3142
|
+
function retryAfterMs(header) {
|
|
3143
|
+
if (header === null) return null;
|
|
3144
|
+
const value = header.trim();
|
|
3145
|
+
if (/^\d+$/.test(value)) return Number.parseInt(value, 10) * 1e3;
|
|
3146
|
+
const at = Date.parse(value);
|
|
3147
|
+
return Number.isNaN(at) ? null : Math.max(0, at - Date.now());
|
|
3148
|
+
}
|
|
3149
|
+
function failureMessage(error) {
|
|
3150
|
+
if (error instanceof TransportError) return `cannot reach the API: ${clean(error.message)}`;
|
|
3151
|
+
return `${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}`;
|
|
3152
|
+
}
|
|
3153
|
+
async function eventsPage(ctx, id, after) {
|
|
3154
|
+
const body = await explained(
|
|
3155
|
+
() => ctx.client.get(`/api/v1/operator/jobs/${id}/events`, {
|
|
3156
|
+
after,
|
|
3157
|
+
limit: EVENTS_PAGE_LIMIT
|
|
3158
|
+
}),
|
|
3159
|
+
{ notFound: missingJob(id) }
|
|
3160
|
+
);
|
|
3161
|
+
const events = objects(body.data, "events");
|
|
3162
|
+
const meta = body.meta;
|
|
3163
|
+
if (typeof meta !== "object" || meta === null) throw new CommandError("The events response has no metadata.");
|
|
3164
|
+
const nextAfter = meta["next_after"];
|
|
3165
|
+
const hasMore = meta["has_more"];
|
|
3166
|
+
const job = meta["job"];
|
|
3167
|
+
if (typeof nextAfter !== "number" || !Number.isInteger(nextAfter) || nextAfter < after) {
|
|
3168
|
+
throw new CommandError("The events response has an invalid next_after.");
|
|
3169
|
+
}
|
|
3170
|
+
if (typeof hasMore !== "boolean") throw new CommandError("The events response has an invalid has_more.");
|
|
3171
|
+
if (!isRecord2(job) || typeof job["state"] !== "string") {
|
|
3172
|
+
throw new CommandError("The events response does not carry the job's state.");
|
|
3173
|
+
}
|
|
3174
|
+
if (hasMore && nextAfter === after) {
|
|
3175
|
+
throw new CommandError("The events response says more exist but did not move its cursor.");
|
|
3176
|
+
}
|
|
3177
|
+
return { events, nextAfter, hasMore, job };
|
|
3178
|
+
}
|
|
3179
|
+
var REFUSAL_HINTS = {
|
|
3180
|
+
operator_disabled: "nothing can be read until Operator is switched back on for this organization",
|
|
3181
|
+
operator_not_permitted: "only the organization's admins and members use Operator; guests and people outside it cannot",
|
|
3182
|
+
actor_required: "the credential acts as no person: sign in with `harness login`, or use an API token a person created",
|
|
3183
|
+
forbidden: "the credential lacks the operator:read scope (`harness login` carries it; an aone_ token must be created with it, bound to this organization), or you are a guest or outside the organization",
|
|
3184
|
+
run_bound_credential: "a run-bound credential cannot read Operator jobs; sign in with `harness login`"
|
|
3185
|
+
};
|
|
3186
|
+
function missingJob(id) {
|
|
3187
|
+
return `no Operator job ${id} you can see: it does not exist, is in another organization, or is not shared with you (or this Harness server has no Operator API yet)`;
|
|
3188
|
+
}
|
|
3189
|
+
async function explained(send, why) {
|
|
3190
|
+
try {
|
|
3191
|
+
return await send();
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
if (!(error instanceof HttpError)) throw error;
|
|
3194
|
+
const code = error.body["error"];
|
|
3195
|
+
let hint;
|
|
3196
|
+
if (error.status === 403 && typeof code === "string") hint = REFUSAL_HINTS[code];
|
|
3197
|
+
if (error.status === 404) hint = why.notFound;
|
|
3198
|
+
if (error.status === 400 && code === "invalid_cursor") hint = "run the list again from the start";
|
|
3199
|
+
if (hint === void 0) throw error;
|
|
3200
|
+
throw new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
function jobsTable(jobs) {
|
|
3204
|
+
if (jobs.length === 0) return "No Operator jobs.\n";
|
|
3205
|
+
const rows = jobs.map((job) => [
|
|
3206
|
+
str5(job["id"]),
|
|
3207
|
+
str5(job["state"]) || "-",
|
|
3208
|
+
str5(job["waiting_on"]) || "-",
|
|
3209
|
+
str5(job["objective_kind"]) || "-",
|
|
3210
|
+
personName(job["owner"]),
|
|
3211
|
+
str5(field2(job, "plan_step", "issue")) || "-",
|
|
3212
|
+
str5(job["updated_at"]) || "-"
|
|
3213
|
+
]);
|
|
3214
|
+
const table2 = table(["ID", "STATE", "WAITING ON", "KIND", "OWNER", "PLAN STEP", "UPDATED"], rows);
|
|
3215
|
+
const approvals = jobs.filter((job) => typeof job["approval_url"] === "string");
|
|
3216
|
+
if (approvals.length === 0) return table2;
|
|
3217
|
+
return table2 + "\nWaiting for approval. Approve in the app; the CLI cannot:\n" + approvals.map((job) => ` ${clean(job["id"])} ${clean(job["approval_url"])}
|
|
3218
|
+
`).join("");
|
|
3219
|
+
}
|
|
3220
|
+
function jobDetail(job) {
|
|
3221
|
+
const waitingOn = str5(job["waiting_on"]);
|
|
3222
|
+
const header = `Operator job ${clean(job["id"])} \xB7 ${clean(job["state"])}` + (waitingOn === "" ? "" : ` \xB7 waiting on ${clean(waitingOn)}`) + "\n";
|
|
3223
|
+
const people = records3(field2(job, "people", "participants"));
|
|
3224
|
+
const fields = [
|
|
3225
|
+
["Kind", str5(job["objective_kind"]) || "-"],
|
|
3226
|
+
["Role", str5(job["viewer_role"]) || null],
|
|
3227
|
+
["Owner", isRecord2(job["owner"]) ? personName(job["owner"]) : null],
|
|
3228
|
+
["Plan step", planStepLine(job["plan_step"])],
|
|
3229
|
+
["Plan", planLine(job["plan"])],
|
|
3230
|
+
["Progress", progressLine(job["progress"])],
|
|
3231
|
+
["Window", windowLine(job["window"])],
|
|
3232
|
+
["Spend", spendLine(job["spend"])],
|
|
3233
|
+
["Verification", str5(field2(job, "verification", "status")) || null],
|
|
3234
|
+
["Effects", effectsLine(job["effects"])],
|
|
3235
|
+
["Skill", skillLine(job["skill"])],
|
|
3236
|
+
["Grant", grantLine(job["grant"], people)],
|
|
3237
|
+
["Created", str5(job["inserted_at"]) || null],
|
|
3238
|
+
["Updated", str5(job["updated_at"]) || null],
|
|
3239
|
+
["Workbench", str5(job["url"]) || null]
|
|
3240
|
+
];
|
|
3241
|
+
return header + labelled(fields) + approvalSection(job["pending_approval"], people) + listSection("Blockers", records3(job["blockers"]), (item) => `${str5(item["kind"])}: ${str5(item["state"])}`) + listSection("Artifacts", records3(job["artifacts"]), (item) => `${str5(item["id"])} ${str5(item["url"])}`) + peopleSection(people) + listSection(
|
|
3242
|
+
"Procedures",
|
|
3243
|
+
records3(job["procedures"]),
|
|
3244
|
+
(procedure) => `${str5(procedure["name"])} v${str5(procedure["current_version"])} ${str5(procedure["id"])}`
|
|
3245
|
+
) + controlsLine(job["viewer_can"]) + "\nThe objective's text is not shown: the API never returns it, because it can quote what Operator read. Open the workbench link to read it.\n";
|
|
3246
|
+
}
|
|
3247
|
+
function labelled(fields, width = 15) {
|
|
3248
|
+
return fields.filter((entry) => entry[1] !== null).map(([label, value]) => ` ${(label + ":").padEnd(width)}${clean(value)}
|
|
3249
|
+
`).join("");
|
|
3250
|
+
}
|
|
3251
|
+
function planStepLine(step) {
|
|
3252
|
+
if (!isRecord2(step)) return null;
|
|
3253
|
+
return `${str5(step["issue"])} (plan ${str5(step["plan_id"])})`;
|
|
3254
|
+
}
|
|
3255
|
+
function planLine(plan) {
|
|
3256
|
+
if (!isRecord2(plan)) return "none yet";
|
|
3257
|
+
const count = plan["capability_count"];
|
|
3258
|
+
return `version ${str5(plan["version"])} \xB7 instructions v${str5(plan["instruction_version"])} \xB7 ${str5(count)} capabilit${count === 1 ? "y" : "ies"}`;
|
|
3259
|
+
}
|
|
3260
|
+
function progressLine(progress) {
|
|
3261
|
+
if (!isRecord2(progress)) return null;
|
|
3262
|
+
const parts = [str5(progress["stage"]), str5(progress["situation"])].filter((part) => part !== "");
|
|
3263
|
+
if (progress["needs_viewer"] === true) parts.push("needs you");
|
|
3264
|
+
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3265
|
+
}
|
|
3266
|
+
function windowLine(window) {
|
|
3267
|
+
if (!isRecord2(window)) return null;
|
|
3268
|
+
const left = duration(window["remaining_ms"]);
|
|
3269
|
+
const budget = duration(window["budget_ms"]);
|
|
3270
|
+
const sequence = window["sequence"];
|
|
3271
|
+
return `${left ?? "?"} left${budget === null ? "" : ` of ${budget}`} \xB7 ${str5(window["state"])}` + (typeof sequence === "number" ? ` (window ${sequence})` : "");
|
|
3272
|
+
}
|
|
3273
|
+
function spendLine(spend) {
|
|
3274
|
+
if (!isRecord2(spend)) return null;
|
|
3275
|
+
const spent = `${str5(spend["spent_cents"]) || "0"} cents spent`;
|
|
3276
|
+
if (typeof spend["plan_budget_cents"] !== "number") return spent;
|
|
3277
|
+
const remaining = spend["plan_remaining_cents"];
|
|
3278
|
+
return `${spent} \xB7 plan budget ${spend["plan_budget_cents"]} cents` + (typeof remaining === "number" ? `, ${remaining} cents left` : "");
|
|
3279
|
+
}
|
|
3280
|
+
function effectsLine(effects) {
|
|
3281
|
+
if (!isRecord2(effects)) return null;
|
|
3282
|
+
return ["staged", "succeeded", "failed", "unknown"].map((key) => `${str5(effects[key]) || "0"} ${key}`).join(" \xB7 ");
|
|
3283
|
+
}
|
|
3284
|
+
function skillLine(skill) {
|
|
3285
|
+
if (!isRecord2(skill)) return null;
|
|
3286
|
+
return `${str5(skill["title"])} (${str5(skill["invocation_name"])}) \xB7 sha256 ${str5(skill["content_sha256"])} \xB7 pinned ${str5(skill["pinned_at"])}`;
|
|
3287
|
+
}
|
|
3288
|
+
function grantLine(grant, people) {
|
|
3289
|
+
if (!isRecord2(grant)) return null;
|
|
3290
|
+
const approvedAt = str5(grant["approved_at"]);
|
|
3291
|
+
const expiresAt = str5(grant["expires_at"]);
|
|
3292
|
+
return `${str5(grant["status"])} v${str5(grant["version"])} \xB7 approved by ${nameOf(grant["approver_user_id"], people)}` + (approvedAt === "" ? "" : ` at ${approvedAt}`) + ` \xB7 ${expiresAt === "" ? "no expiry" : `expires ${expiresAt}`}`;
|
|
3293
|
+
}
|
|
3294
|
+
function approvalSection(pending, people) {
|
|
3295
|
+
if (!isRecord2(pending)) return "";
|
|
3296
|
+
const approvers = Array.isArray(pending["approver_user_ids"]) ? pending["approver_user_ids"].map((id) => nameOf(id, people)).join(", ") : "";
|
|
3297
|
+
const canApprove = pending["viewer_can_approve"] === true ? "you can approve" : "you cannot approve it";
|
|
3298
|
+
return "\nWaiting for approval. Approve it in the app; the CLI cannot:\n" + labelled([
|
|
3299
|
+
[
|
|
3300
|
+
"Change set",
|
|
3301
|
+
`${str5(pending["manifest_id"])} v${str5(pending["version"])} \xB7 ${str5(pending["action_count"])} actions \xB7 up to ${str5(pending["max_effect_count"])} effects \xB7 plan version ${str5(pending["plan_version"])}`
|
|
3302
|
+
],
|
|
3303
|
+
["Digest", str5(pending["digest"]) || null],
|
|
3304
|
+
["Approvers", approvers === "" ? canApprove : `${approvers} \xB7 ${canApprove}`],
|
|
3305
|
+
["Approve at", str5(pending["url"]) || null]
|
|
3306
|
+
]);
|
|
3307
|
+
}
|
|
3308
|
+
function peopleSection(people) {
|
|
3309
|
+
if (people.length === 0) return "";
|
|
3310
|
+
const rows = people.map((person) => [
|
|
3311
|
+
str5(person["name"]) || "(no name)",
|
|
3312
|
+
str5(person["role"]),
|
|
3313
|
+
str5(person["user_id"])
|
|
3314
|
+
]);
|
|
3315
|
+
const lines = table(["", "", ""], rows).split("\n").slice(1, -1);
|
|
3316
|
+
return "\nPeople:\n" + lines.map((line) => ` ${line}
|
|
3317
|
+
`).join("");
|
|
3318
|
+
}
|
|
3319
|
+
function changesText(id, job, changeSet) {
|
|
3320
|
+
if (changeSet === null) return `No pending change set: Operator has proposed nothing for job ${id} yet.
|
|
3321
|
+
`;
|
|
3322
|
+
const approvalUrl = str5(job["approval_url"]);
|
|
3323
|
+
const rows = records3(changeSet["actions"]).map((action) => [
|
|
3324
|
+
str5(action["display_order"]) || "-",
|
|
3325
|
+
str5(action["action_type"]),
|
|
3326
|
+
str5(action["action_version"]),
|
|
3327
|
+
str5(action["effect_count"]),
|
|
3328
|
+
(Array.isArray(action["required_capability_ids"]) ? action["required_capability_ids"].map(str5).join(", ") : "") || "-",
|
|
3329
|
+
str5(action["digest"]),
|
|
3330
|
+
str5(action["id"])
|
|
3331
|
+
]);
|
|
3332
|
+
const actions = rows.length === 0 ? "\nActions: none\n" : "\nActions:\n" + table(["#", "ACTION", "VERSION", "EFFECTS", "CAPABILITIES", "DIGEST", "ID"], rows).split("\n").slice(0, -1).map((line) => ` ${line}
|
|
3333
|
+
`).join("");
|
|
3334
|
+
return `Change set of Operator job ${id} \xB7 ${clean(changeSet["status"])}
|
|
3335
|
+
` + labelled([
|
|
3336
|
+
[
|
|
3337
|
+
"Change set",
|
|
3338
|
+
`${str5(changeSet["manifest_id"])} v${str5(changeSet["version"])} \xB7 plan version ${str5(changeSet["plan_version"])} \xB7 up to ${str5(changeSet["max_effect_count"])} effects`
|
|
3339
|
+
],
|
|
3340
|
+
["Digest", str5(changeSet["digest"]) || null],
|
|
3341
|
+
approvalUrl === "" ? ["Workbench", str5(job["url"]) || null] : ["Approve at", approvalUrl]
|
|
3342
|
+
]) + actions + "\nValues and targets are not shown: review them in the app at the " + (approvalUrl === "" ? "workbench link.\n" : "approval link. The CLI cannot approve.\n");
|
|
3343
|
+
}
|
|
3344
|
+
function peopleText(id, people) {
|
|
3345
|
+
const one = (entry) => isRecord2(entry) ? str5(entry["name"]) || str5(entry["user_id"]) : "-";
|
|
3346
|
+
const many = (entries) => (Array.isArray(entries) ? entries.map(one).join(", ") : "") || "none";
|
|
3347
|
+
return `People on Operator job ${id}:
|
|
3348
|
+
` + labelled(
|
|
3349
|
+
[
|
|
3350
|
+
["Requester", one(people["requester"])],
|
|
3351
|
+
["Execution principal", one(people["execution_principal"])],
|
|
3352
|
+
["Owner", one(people["owner"])],
|
|
3353
|
+
["Approvers", many(people["approvers"])],
|
|
3354
|
+
["Collaborators", many(people["collaborators"])]
|
|
3355
|
+
],
|
|
3356
|
+
22
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
function listSection(title, items, line) {
|
|
3360
|
+
if (items.length === 0) return "";
|
|
3361
|
+
return `
|
|
3362
|
+
${title}:
|
|
3363
|
+
` + items.map((item) => ` ${clean(line(item))}
|
|
3364
|
+
`).join("");
|
|
3365
|
+
}
|
|
3366
|
+
function controlsLine(viewerCan) {
|
|
3367
|
+
const controls = isRecord2(viewerCan) ? ["steer", "cancel", "continue", "pause", "save_procedure"].filter((key) => viewerCan[key] === true) : [];
|
|
3368
|
+
return `
|
|
3369
|
+
You can, in the workbench: ${controls.length === 0 ? "nothing right now" : controls.join(", ")}
|
|
3370
|
+
`;
|
|
3371
|
+
}
|
|
3372
|
+
function eventLine(event) {
|
|
3373
|
+
const actor = isRecord2(event["actor"]) ? event["actor"] : {};
|
|
3374
|
+
const actorKind = safeValue(actor["kind"]) || "?";
|
|
3375
|
+
const actorId = safeValue(actor["delegated_grant_id"] ?? actor["user_id"]);
|
|
3376
|
+
const data = isRecord2(event["data"]) ? Object.entries(event["data"]).map(([key, value]) => `${clean(key)}=${safeValue(value) || "\u2026"}`) : [];
|
|
3377
|
+
return `#${clean(event["sequence"])} ${clean(event["occurred_at"])} ${clean(event["kind"])} ${actorId === "" ? actorKind : `${actorKind} ${actorId}`}` + (data.length === 0 ? "" : ` ${data.join(" ")}`) + "\n";
|
|
3378
|
+
}
|
|
3379
|
+
function eventJson(event) {
|
|
3380
|
+
const actor = isRecord2(event["actor"]) ? event["actor"] : {};
|
|
3381
|
+
const data = isRecord2(event["data"]) ? event["data"] : {};
|
|
3382
|
+
return {
|
|
3383
|
+
sequence: typeof event["sequence"] === "number" ? event["sequence"] : null,
|
|
3384
|
+
kind: safeValue(event["kind"]) || null,
|
|
3385
|
+
occurred_at: typeof event["occurred_at"] === "string" && TIMESTAMP.test(event["occurred_at"]) ? event["occurred_at"] : null,
|
|
3386
|
+
actor: Object.fromEntries(
|
|
3387
|
+
["kind", "user_id", "delegated_grant_id"].filter((key) => key in actor && (actor[key] === null || safeValue(actor[key]) !== "")).map((key) => [key, actor[key]])
|
|
3388
|
+
),
|
|
3389
|
+
data: Object.fromEntries(Object.entries(data).filter(([key, value]) => CODE.test(key) && safeValue(value) !== ""))
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
function jobStateJson(job) {
|
|
3393
|
+
return Object.fromEntries(
|
|
3394
|
+
["state", "waiting_on", "updated_at", "url", "approval_url"].filter((key) => key in job).map((key) => [key, job[key]])
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
function stateLine(previous, state, waitingOn) {
|
|
3398
|
+
const now = clean(state) + (waitingOn === null ? "" : `, waiting on ${clean(waitingOn)}`);
|
|
3399
|
+
return previous === null || previous === state ? `state: ${now}
|
|
3400
|
+
` : `state: ${clean(previous)} \u2192 ${now}
|
|
3401
|
+
`;
|
|
3402
|
+
}
|
|
3403
|
+
function outcomeText(id, outcome, job) {
|
|
3404
|
+
const url = clean(job["url"]);
|
|
3405
|
+
switch (outcome) {
|
|
3406
|
+
case "completed":
|
|
3407
|
+
return `Job ${id} completed.
|
|
3408
|
+
Workbench: ${url}
|
|
3409
|
+
`;
|
|
3410
|
+
case "failed":
|
|
3411
|
+
return `Job ${id} failed.
|
|
3412
|
+
Workbench: ${url}
|
|
3413
|
+
`;
|
|
3414
|
+
case "cancelled":
|
|
3415
|
+
return `Job ${id} was cancelled.
|
|
3416
|
+
Workbench: ${url}
|
|
3417
|
+
`;
|
|
3418
|
+
case "waiting": {
|
|
3419
|
+
const waitingOn = job["waiting_on"];
|
|
3420
|
+
if (waitingOn === "approval") {
|
|
3421
|
+
const link = clean(job["approval_url"]) || url;
|
|
3422
|
+
return `Job ${id} is waiting for approval. Approve it in the app; the CLI cannot:
|
|
3423
|
+
${link}
|
|
3424
|
+
`;
|
|
3425
|
+
}
|
|
3426
|
+
const what = waitingOn === "person" ? "a person" : clean(waitingOn);
|
|
3427
|
+
return `Job ${id} is waiting on ${what}; a person has to act in the workbench:
|
|
3428
|
+
${url}
|
|
3429
|
+
`;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
function withheld(job) {
|
|
3434
|
+
return Object.fromEntries(
|
|
3435
|
+
Object.entries(job).filter(([key]) => !WITHHELD_FIELDS.has(key) && !key.startsWith("knowledge"))
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
function pick(record, keys) {
|
|
3439
|
+
return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
|
|
3440
|
+
}
|
|
3441
|
+
function objects(value, what) {
|
|
3442
|
+
if (!Array.isArray(value) || !value.every(isRecord2)) {
|
|
3443
|
+
throw new CommandError(`The ${what} response data is not a list of objects.`);
|
|
3444
|
+
}
|
|
3445
|
+
return value;
|
|
3446
|
+
}
|
|
3447
|
+
function plural(count, noun) {
|
|
3448
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
3449
|
+
}
|
|
3450
|
+
function uniqueById(jobs) {
|
|
3451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3452
|
+
return jobs.filter((job) => {
|
|
3453
|
+
const id = str5(job["id"]);
|
|
3454
|
+
if (seen.has(id)) return false;
|
|
3455
|
+
seen.add(id);
|
|
3456
|
+
return true;
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
function personName(person) {
|
|
3460
|
+
if (!isRecord2(person)) return "-";
|
|
3461
|
+
return str5(person["name"]) || str5(person["user_id"]) || "-";
|
|
3462
|
+
}
|
|
3463
|
+
function nameOf(userId, people) {
|
|
3464
|
+
const id = str5(userId);
|
|
3465
|
+
const person = people.find((candidate) => candidate["user_id"] === id);
|
|
3466
|
+
return (person === void 0 ? "" : str5(person["name"])) || id || "?";
|
|
3467
|
+
}
|
|
3468
|
+
function duration(ms) {
|
|
3469
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return null;
|
|
3470
|
+
const seconds = Math.max(0, Math.floor(ms / 1e3));
|
|
3471
|
+
const hours = Math.floor(seconds / 3600);
|
|
3472
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
3473
|
+
const rest = seconds % 60;
|
|
3474
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
3475
|
+
if (minutes > 0) return `${minutes}m ${String(rest).padStart(2, "0")}s`;
|
|
3476
|
+
return `${rest}s`;
|
|
3477
|
+
}
|
|
3478
|
+
function safeValue(value) {
|
|
3479
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
3480
|
+
if (typeof value === "boolean") return String(value);
|
|
3481
|
+
if (typeof value === "string" && CODE.test(value)) return value;
|
|
3482
|
+
return "";
|
|
3483
|
+
}
|
|
3484
|
+
function field2(record, ...keys) {
|
|
3485
|
+
let current = record;
|
|
3486
|
+
for (const key of keys) {
|
|
3487
|
+
if (!isRecord2(current)) return null;
|
|
3488
|
+
current = current[key];
|
|
3489
|
+
}
|
|
3490
|
+
return current ?? null;
|
|
3491
|
+
}
|
|
3492
|
+
function records3(value) {
|
|
3493
|
+
return Array.isArray(value) ? value.filter(isRecord2) : [];
|
|
3494
|
+
}
|
|
3495
|
+
function isRecord2(value) {
|
|
3496
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3497
|
+
}
|
|
3498
|
+
function str5(value) {
|
|
3499
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// src/commands/operator-write.ts
|
|
3503
|
+
var ACCESS_HINTS = {
|
|
3504
|
+
...REFUSAL_HINTS,
|
|
3505
|
+
operator_disabled: "nothing can be changed until Operator is switched back on for this organization",
|
|
3506
|
+
actor_required: "the credential acts as no person: use a browser login, or an API token a person created",
|
|
3507
|
+
forbidden: "the credential lacks the operator:write scope (a browser login carries it; an aone_ token must be created with it, bound to this organization), or you are a guest or outside the organization",
|
|
3508
|
+
run_bound_credential: "a run-bound credential cannot change Operator jobs; use a browser login"
|
|
3509
|
+
};
|
|
3510
|
+
var BAD_REQUEST_HINT = "nothing changed: the server refused a field this CLI sent, so this CLI and the server disagree about the request \u2014 update the CLI";
|
|
3511
|
+
var NEVER_SENT = /* @__PURE__ */ new Set([
|
|
3512
|
+
"ENOTFOUND",
|
|
3513
|
+
"EAI_AGAIN",
|
|
3514
|
+
"EAI_FAIL",
|
|
3515
|
+
"ECONNREFUSED",
|
|
3516
|
+
"ENETUNREACH",
|
|
3517
|
+
"EHOSTUNREACH",
|
|
3518
|
+
"EHOSTDOWN",
|
|
3519
|
+
"EADDRNOTAVAIL",
|
|
3520
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
3521
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
3522
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
3523
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
3524
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
3525
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
3526
|
+
"CERT_HAS_EXPIRED",
|
|
3527
|
+
"CERT_NOT_YET_VALID",
|
|
3528
|
+
"CERT_REVOKED",
|
|
3529
|
+
"CERT_UNTRUSTED",
|
|
3530
|
+
"CERT_REJECTED",
|
|
3531
|
+
"HOSTNAME_MISMATCH",
|
|
3532
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
3533
|
+
"ERR_SSL_WRONG_VERSION_NUMBER"
|
|
3534
|
+
]);
|
|
3535
|
+
function neverSent(error) {
|
|
3536
|
+
return !(error instanceof MalformedResponseError) && error.code !== null && NEVER_SENT.has(error.code);
|
|
3537
|
+
}
|
|
3538
|
+
function outcomeUnknown(why, write2) {
|
|
3539
|
+
return new CommandError(
|
|
3540
|
+
`Outcome unknown: ${why}, so ${write2.mayHave}. It is not retried; check ${write2.check} before trying again.`
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
async function sendOnce(ctx, path2, body, write2) {
|
|
3544
|
+
try {
|
|
3545
|
+
return await ctx.client.post(path2, body, { actor: false });
|
|
3546
|
+
} catch (error) {
|
|
3547
|
+
if (error instanceof TransportError) {
|
|
3548
|
+
if (neverSent(error)) {
|
|
3549
|
+
throw new TransportError(
|
|
3550
|
+
`${error.message} \u2014 the request never reached the server, so nothing changed`,
|
|
3551
|
+
error.code
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
throw outcomeUnknown(
|
|
3555
|
+
error instanceof MalformedResponseError ? `the answer could not be read (${error.message})` : `the request failed (${error.message}) after it may have reached the server`,
|
|
3556
|
+
write2
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
if (error instanceof HttpError && !(error instanceof UpgradeRequiredError) && error.status >= 500) {
|
|
3560
|
+
throw new HttpError(
|
|
3561
|
+
error.status,
|
|
3562
|
+
`${error.message} \u2014 outcome unknown: ${write2.mayHave}. It is not retried; check ${write2.check} before trying again`,
|
|
3563
|
+
error.body,
|
|
3564
|
+
error.retryAfter
|
|
3565
|
+
);
|
|
3566
|
+
}
|
|
3567
|
+
throw error;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
function explainRefusal(error, notFound2, hints) {
|
|
3571
|
+
if (!(error instanceof HttpError) || error instanceof UpgradeRequiredError) return error;
|
|
3572
|
+
const code = typeof error.body["error"] === "string" ? error.body["error"] : "";
|
|
3573
|
+
const own = Object.hasOwn(hints, code) ? hints[code] : void 0;
|
|
3574
|
+
let hint;
|
|
3575
|
+
if (error.status >= 500) hint = own;
|
|
3576
|
+
else if (error.status === 404) hint = notFound2;
|
|
3577
|
+
else if (own !== void 0) hint = own;
|
|
3578
|
+
else if (error.status === 403 && Object.hasOwn(ACCESS_HINTS, code)) hint = ACCESS_HINTS[code];
|
|
3579
|
+
else if (error.status === 400 && code === "bad_request") hint = BAD_REQUEST_HINT;
|
|
3580
|
+
if (hint === void 0) return error;
|
|
3581
|
+
const message = error.status >= 500 && typeof error.body["message"] === "string" ? error.body["message"] : error.message;
|
|
3582
|
+
return new HttpError(error.status, `${message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
// src/commands/operator-continue.ts
|
|
3586
|
+
async function jobContinue(ctx, id) {
|
|
3587
|
+
requireJobId(id);
|
|
3588
|
+
const write2 = { mayHave: "the job may have been continued", check: `\`job ${id}\`` };
|
|
3589
|
+
let data;
|
|
3590
|
+
try {
|
|
3591
|
+
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${id}/continue`, {}, write2)).data;
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
throw explainRefusal(error, notFound(id), refusals(id));
|
|
3594
|
+
}
|
|
3595
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3596
|
+
throw outcomeUnknown("the answer carried no job", write2);
|
|
3597
|
+
}
|
|
3598
|
+
const job = withheld(data);
|
|
3599
|
+
return { json: job, human: `Continued Operator job ${id}.
|
|
3600
|
+
|
|
3601
|
+
` + jobDetail(job) };
|
|
3602
|
+
}
|
|
3603
|
+
function notFound(id) {
|
|
3604
|
+
return `${missingJob(id)}. If you could see the job until now, it may have gone out of sight just as it was continued: check \`job ${id}\` before continuing it again`;
|
|
3605
|
+
}
|
|
3606
|
+
function refusals(id) {
|
|
3607
|
+
return {
|
|
3608
|
+
principal_required: "only the person the job runs as (its execution principal) can continue it",
|
|
3609
|
+
invalid_state: `the job has nothing to continue in its current state; \`job ${id}\` shows where it stands`,
|
|
3610
|
+
reconciliation_required: "reconcile its partial effects in the app first, from the job's workbench link",
|
|
3611
|
+
principal_inactive: "the person the job runs as lost the rights to run it, so it can't continue until those are restored",
|
|
3612
|
+
deliveries_unsettled: `a delivery is still sending, or its outcome is unknown: if \`job ${id}\` shows it waiting on delivery, settle it in the app; otherwise continue it once the delivery has sent`
|
|
3613
|
+
};
|
|
3614
|
+
}
|
|
3615
|
+
|
|
2218
3616
|
// src/core/textinput.ts
|
|
2219
3617
|
import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
2220
3618
|
import { isatty } from "tty";
|
|
@@ -2334,14 +3732,379 @@ function describe(error) {
|
|
|
2334
3732
|
return error instanceof Error ? error.message : String(error);
|
|
2335
3733
|
}
|
|
2336
3734
|
|
|
3735
|
+
// src/commands/operator-controls.ts
|
|
3736
|
+
var STEER_MAX_CHARACTERS = 1e4;
|
|
3737
|
+
var DONE = { steer: "steered", cancel: "cancelled", pause: "paused" };
|
|
3738
|
+
function requireInstruction(instruction) {
|
|
3739
|
+
const trimmed = instruction.trim();
|
|
3740
|
+
if (trimmed === "") throw new UsageError("the instruction is empty \u2014 say what the job should change");
|
|
3741
|
+
const length = graphemeLength(trimmed);
|
|
3742
|
+
if (length > STEER_MAX_CHARACTERS) {
|
|
3743
|
+
throw new UsageError(
|
|
3744
|
+
`the instruction is ${length} characters once trimmed; a job takes at most ${STEER_MAX_CHARACTERS}`
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
return trimmed;
|
|
3748
|
+
}
|
|
3749
|
+
function instructionFrom(words, file) {
|
|
3750
|
+
if (file === void 0) return requireInstruction(words.join(" "));
|
|
3751
|
+
if (words.length > 0) {
|
|
3752
|
+
throw new UsageError(
|
|
3753
|
+
"instruction words and --instruction-file both given \u2014 pass one; there is no precedence between them"
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
return requireInstruction(resolveTextSources([{ name: "instruction", inline: void 0, file }])["instruction"] ?? "");
|
|
3757
|
+
}
|
|
3758
|
+
async function jobSteer(ctx, id, instruction) {
|
|
3759
|
+
requireJobId(id);
|
|
3760
|
+
const job = await control(ctx, id, "steer", { instruction: requireInstruction(instruction) });
|
|
3761
|
+
return {
|
|
3762
|
+
json: job,
|
|
3763
|
+
human: `Steered Operator job ${id}: the instruction is posted in its room, attributed to you. Work it changes needs a fresh approval (earlier approvals no longer apply), and a run in progress stops to take it.
|
|
3764
|
+
|
|
3765
|
+
` + jobDetail(job)
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
async function jobCancel(ctx, id) {
|
|
3769
|
+
requireJobId(id);
|
|
3770
|
+
const job = await control(ctx, id, "cancel", {});
|
|
3771
|
+
const effects = effectsLine(job["effects"]);
|
|
3772
|
+
return {
|
|
3773
|
+
json: job,
|
|
3774
|
+
human: `Cancelled Operator job ${id}. ` + (effects === null ? "Committed effects stay committed; the answer carried no counts.\n" : `Committed effects stay committed: ${clean(effects)} (staged effects that never ran count as unknown).
|
|
3775
|
+
`) + "\n" + jobDetail(job)
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
async function jobPause(ctx, id) {
|
|
3779
|
+
requireJobId(id);
|
|
3780
|
+
const job = await control(ctx, id, "pause", {});
|
|
3781
|
+
return {
|
|
3782
|
+
json: job,
|
|
3783
|
+
human: `Paused Operator job ${id}. Its active-time budget is kept; continue it to run again.
|
|
3784
|
+
|
|
3785
|
+
` + jobDetail(job)
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
async function control(ctx, id, verb, body) {
|
|
3789
|
+
const write2 = { mayHave: `the job may have been ${DONE[verb]}`, check: `\`job ${id}\`` };
|
|
3790
|
+
let data;
|
|
3791
|
+
try {
|
|
3792
|
+
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${id}/${verb}`, body, write2)).data;
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
throw explained2(error, id, verb);
|
|
3795
|
+
}
|
|
3796
|
+
if (!isRecord3(data)) throw outcomeUnknown("the answer carried no job", write2);
|
|
3797
|
+
return withheld(data);
|
|
3798
|
+
}
|
|
3799
|
+
function explained2(error, id, verb) {
|
|
3800
|
+
if (!(error instanceof HttpError) || error.status >= 500) return error;
|
|
3801
|
+
const hint = refusalHint(error, id, verb);
|
|
3802
|
+
if (hint === void 0) return error;
|
|
3803
|
+
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
3804
|
+
}
|
|
3805
|
+
function refusalHint(error, id, verb) {
|
|
3806
|
+
const code = typeof error.body["error"] === "string" ? error.body["error"] : "";
|
|
3807
|
+
const job = `\`job ${id}\``;
|
|
3808
|
+
switch (error.status) {
|
|
3809
|
+
case 400:
|
|
3810
|
+
return code === "bad_request" ? BAD_REQUEST_HINT : void 0;
|
|
3811
|
+
case 403:
|
|
3812
|
+
if (code !== "principal_required") return ACCESS_HINTS[code];
|
|
3813
|
+
return verb === "pause" ? "only the job's execution principal may pause it; as its owner or a collaborator you can steer it instead, which stops its current run" : `only the job's execution principal may ${verb} it; ask them to`;
|
|
3814
|
+
case 404:
|
|
3815
|
+
return missingJob(id);
|
|
3816
|
+
case 409:
|
|
3817
|
+
return conflictHint(code, verb, job);
|
|
3818
|
+
case 422:
|
|
3819
|
+
if (code === "invalid_instruction") {
|
|
3820
|
+
return `pass the instruction as text of 1 to ${STEER_MAX_CHARACTERS} characters once trimmed`;
|
|
3821
|
+
}
|
|
3822
|
+
return code === "validation_error" && verb === "steer" ? "the job's room refused the instruction, so nothing was posted" : void 0;
|
|
3823
|
+
default:
|
|
3824
|
+
return void 0;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
function conflictHint(code, verb, job) {
|
|
3828
|
+
switch (code) {
|
|
3829
|
+
case "invalid_state":
|
|
3830
|
+
return {
|
|
3831
|
+
steer: `the app doesn't offer Steer for the job now: it has finished, failed, waits for approval or needs reconciliation \u2014 ${job} shows where it stands`,
|
|
3832
|
+
cancel: `the job has already ended \u2014 ${job} shows how`,
|
|
3833
|
+
pause: `only a running job can be paused \u2014 ${job} shows where it stands`
|
|
3834
|
+
}[verb];
|
|
3835
|
+
case "reconciliation_required":
|
|
3836
|
+
return `its partial effects need reconciling first: reconcile them in the app (${job} has the workbench link)`;
|
|
3837
|
+
case "deliveries_unsettled":
|
|
3838
|
+
return `a delivery is still sending, or its outcome is unknown: if ${job} shows it waiting on delivery, settle that in the app (the workbench link is there); otherwise ${verb} it again once the delivery has sent`;
|
|
3839
|
+
case "principal_inactive":
|
|
3840
|
+
return "nothing changed: the job's execution principal needs their admin or member role back first";
|
|
3841
|
+
case "cancellation_scope_changed":
|
|
3842
|
+
return `the job was not cancelled; read it with ${job} and decide again`;
|
|
3843
|
+
default:
|
|
3844
|
+
return void 0;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
function isRecord3(value) {
|
|
3848
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
// src/commands/operator-procedures.ts
|
|
3852
|
+
var PROCEDURE_FIELDS = [
|
|
3853
|
+
"id",
|
|
3854
|
+
"name",
|
|
3855
|
+
"job_id",
|
|
3856
|
+
"job_url",
|
|
3857
|
+
"current_version",
|
|
3858
|
+
"parameter_schema",
|
|
3859
|
+
"versions",
|
|
3860
|
+
"inserted_at",
|
|
3861
|
+
"updated_at"
|
|
3862
|
+
];
|
|
3863
|
+
var PROCEDURE_VERSION_FIELDS = [
|
|
3864
|
+
"version",
|
|
3865
|
+
"recipe_digest",
|
|
3866
|
+
"capability_ids",
|
|
3867
|
+
"action_roster",
|
|
3868
|
+
"inserted_at"
|
|
3869
|
+
];
|
|
3870
|
+
var RUN_NOTE = "A run is a new job: fresh inputs, your current rights and a change set that needs its own approval. Nothing of the saved job carries over. Approving happens in the app; the CLI cannot approve.\n";
|
|
3871
|
+
function requireProcedureId(id) {
|
|
3872
|
+
if (!isUuid(id)) throw new UsageError(`not an Operator procedure id: ${id} (expected the procedure's UUID)`);
|
|
3873
|
+
}
|
|
3874
|
+
function missingProcedure(id) {
|
|
3875
|
+
return `no Operator procedure ${id} you can see: it does not exist or was deleted, is in another organization, was saved from a job you can't see, or was saved for the whole organization rather than a job (or this Harness server has no procedures API yet)`;
|
|
3876
|
+
}
|
|
3877
|
+
function requireProcedureName(name) {
|
|
3878
|
+
if (name === void 0 || name.trim() === "") {
|
|
3879
|
+
throw new UsageError('a procedure needs a name: procedure save <job-id> --name "\u2026"');
|
|
3880
|
+
}
|
|
3881
|
+
return name;
|
|
3882
|
+
}
|
|
3883
|
+
async function procedureSave(ctx, jobId, name) {
|
|
3884
|
+
requireJobId(jobId);
|
|
3885
|
+
const named = requireProcedureName(name);
|
|
3886
|
+
const write2 = {
|
|
3887
|
+
mayHave: "a procedure may have been saved",
|
|
3888
|
+
check: `the job's procedures with \`job ${jobId}\``
|
|
3889
|
+
};
|
|
3890
|
+
let data;
|
|
3891
|
+
try {
|
|
3892
|
+
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${jobId}/procedures`, { name: named }, write2)).data;
|
|
3893
|
+
} catch (error) {
|
|
3894
|
+
throw explainRefusal(error, missingJob(jobId), {
|
|
3895
|
+
owner_required: "only the job's owner can save it as a procedure; ask them to save it",
|
|
3896
|
+
not_saveable: "only a job that completed with its work verified, and has work to repeat, can be saved",
|
|
3897
|
+
validation_error: "the name must be plain text, without control characters"
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
if (!isRecord4(data) || typeof data["id"] !== "string" || data["id"] === "") {
|
|
3901
|
+
throw outcomeUnknown("the answer named no procedure", write2);
|
|
3902
|
+
}
|
|
3903
|
+
const saved = `Saved job ${jobId} as procedure ${clean(data["id"])}`;
|
|
3904
|
+
if (!Array.isArray(data["versions"])) {
|
|
3905
|
+
return {
|
|
3906
|
+
json: pick2(data, ["id", "url"]),
|
|
3907
|
+
human: `${saved}, but it can't be read back as you.
|
|
3908
|
+
` + labelled([["Workbench", str6(data["url"]) || null]])
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
const procedure = procedureJson(data);
|
|
3912
|
+
return { json: procedure, human: `${saved}.
|
|
3913
|
+
|
|
3914
|
+
` + procedureDetail(procedure) };
|
|
3915
|
+
}
|
|
3916
|
+
async function procedureShow(ctx, id) {
|
|
3917
|
+
requireProcedureId(id);
|
|
3918
|
+
const body = await explained(() => ctx.client.get(`/api/v1/operator/procedures/${id}`), {
|
|
3919
|
+
notFound: missingProcedure(id)
|
|
3920
|
+
});
|
|
3921
|
+
if (!isRecord4(body.data)) throw new CommandError("The procedure response carried no procedure.");
|
|
3922
|
+
const procedure = procedureJson(body.data);
|
|
3923
|
+
return { json: procedure, human: procedureDetail(procedure) };
|
|
3924
|
+
}
|
|
3925
|
+
function prepareProcedureRun(id, opts) {
|
|
3926
|
+
requireProcedureId(id);
|
|
3927
|
+
const body = {};
|
|
3928
|
+
const version2 = runVersion(opts.version);
|
|
3929
|
+
if (version2 !== void 0) body["version"] = version2;
|
|
3930
|
+
const given = [
|
|
3931
|
+
opts.param !== void 0 && opts.param.length > 0 ? "--param" : null,
|
|
3932
|
+
opts.paramsFile !== void 0 ? "--params-file" : null,
|
|
3933
|
+
opts.parameters !== void 0 ? "parameters" : null
|
|
3934
|
+
].filter((source) => source !== null);
|
|
3935
|
+
if (given.length > 1) {
|
|
3936
|
+
throw new UsageError(`${given.join(" and ")} both given \u2014 pass the parameters one way; there is no precedence`);
|
|
3937
|
+
}
|
|
3938
|
+
if (opts.parameters !== void 0 && !isRecord4(opts.parameters)) {
|
|
3939
|
+
throw new UsageError("parameters must be one object of names and values");
|
|
3940
|
+
}
|
|
3941
|
+
const fromFlags = opts.param !== void 0 && opts.param.length > 0 ? parseParams(opts.param) : void 0;
|
|
3942
|
+
if (opts.group !== void 0) body["group_id"] = bareId(opts.group, "--group");
|
|
3943
|
+
if (opts.source !== void 0) body["source_resource_id"] = bareId(opts.source, "--source");
|
|
3944
|
+
const parameters = fromFlags ?? opts.parameters ?? (opts.paramsFile === void 0 ? void 0 : readParamsFile(opts.paramsFile));
|
|
3945
|
+
if (parameters !== void 0) body["parameters"] = parameters;
|
|
3946
|
+
return { id, body };
|
|
3947
|
+
}
|
|
3948
|
+
async function startProcedureRun(ctx, request) {
|
|
3949
|
+
const { id } = request;
|
|
3950
|
+
const write2 = { mayHave: "a job may have been started", check: "`jobs` for the new job" };
|
|
3951
|
+
let data;
|
|
3952
|
+
try {
|
|
3953
|
+
data = (await sendOnce(ctx, `/api/v1/operator/procedures/${id}/runs`, request.body, write2)).data;
|
|
3954
|
+
} catch (error) {
|
|
3955
|
+
throw explainRefusal(error, missingProcedure(id), {
|
|
3956
|
+
version_conflict: `the procedure has a newer current version (\`procedure show ${id}\` names it): run that one, or run without a version to take the current one`,
|
|
3957
|
+
concurrent_change: `read the procedure again (\`procedure show ${id}\`) before running it again`,
|
|
3958
|
+
invalid_rerun: `check the parameters against the procedure's parameter schema (\`procedure show ${id}\`), and that your current rights cover the run`,
|
|
3959
|
+
// The server refuses with it before the run starts.
|
|
3960
|
+
service_unavailable: "nothing was started; try again later"
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
if (!isRecord4(data) || typeof data["id"] !== "string" || data["id"] === "") {
|
|
3964
|
+
throw outcomeUnknown("the answer named no job", write2);
|
|
3965
|
+
}
|
|
3966
|
+
const started = `Started Operator job ${clean(data["id"])} from procedure ${id}`;
|
|
3967
|
+
if (typeof data["state"] !== "string") {
|
|
3968
|
+
return {
|
|
3969
|
+
json: pick2(data, ["id", "url"]),
|
|
3970
|
+
human: `${started}, but it can't be read back as you.
|
|
3971
|
+
` + labelled([["Workbench", str6(data["url"]) || null]]) + "\n" + RUN_NOTE
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
const job = withheld(data);
|
|
3975
|
+
const waitingOn = str6(job["waiting_on"]);
|
|
3976
|
+
return {
|
|
3977
|
+
json: job,
|
|
3978
|
+
human: `${started}.
|
|
3979
|
+
` + labelled([
|
|
3980
|
+
["State", str6(job["state"]) + (waitingOn === "" ? "" : ` \xB7 waiting on ${waitingOn}`)],
|
|
3981
|
+
["Workbench", str6(job["url"]) || null],
|
|
3982
|
+
["Approve at", str6(job["approval_url"]) || null]
|
|
3983
|
+
]) + "\n" + RUN_NOTE
|
|
3984
|
+
};
|
|
3985
|
+
}
|
|
3986
|
+
async function procedureRun(ctx, id, opts) {
|
|
3987
|
+
return startProcedureRun(ctx, prepareProcedureRun(id, opts));
|
|
3988
|
+
}
|
|
3989
|
+
function runVersion(version2) {
|
|
3990
|
+
if (version2 === void 0) return void 0;
|
|
3991
|
+
const text = String(version2).trim();
|
|
3992
|
+
const parsed = typeof version2 === "number" ? version2 : /^\d+$/.test(text) ? Number(text) : Number.NaN;
|
|
3993
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
3994
|
+
throw new UsageError(`--version takes a positive integer, the version expected to be current (got ${text})`);
|
|
3995
|
+
}
|
|
3996
|
+
return parsed;
|
|
3997
|
+
}
|
|
3998
|
+
function parseParams(entries) {
|
|
3999
|
+
const parsed = [];
|
|
4000
|
+
const names = /* @__PURE__ */ new Set();
|
|
4001
|
+
for (const entry of entries) {
|
|
4002
|
+
const equals = entry.indexOf("=");
|
|
4003
|
+
if (equals === -1) throw new UsageError(`--param ${entry}: expected name=value, or name:=<json> for a JSON value`);
|
|
4004
|
+
const typed = equals > 0 && entry[equals - 1] === ":";
|
|
4005
|
+
const name = entry.slice(0, typed ? equals - 1 : equals);
|
|
4006
|
+
const raw = entry.slice(equals + 1);
|
|
4007
|
+
if (name.trim() === "") throw new UsageError(`--param ${entry}: the parameter has no name`);
|
|
4008
|
+
if (name !== name.trim()) {
|
|
4009
|
+
throw new UsageError(`--param ${entry}: the name starts or ends with whitespace \u2014 write it without`);
|
|
4010
|
+
}
|
|
4011
|
+
if (names.has(name)) throw new UsageError(`--param ${name} given twice \u2014 pass each parameter once`);
|
|
4012
|
+
names.add(name);
|
|
4013
|
+
parsed.push([name, typed ? jsonValue(raw, `--param ${entry}`) : raw]);
|
|
4014
|
+
}
|
|
4015
|
+
return Object.fromEntries(parsed);
|
|
4016
|
+
}
|
|
4017
|
+
function jsonValue(text, what) {
|
|
4018
|
+
try {
|
|
4019
|
+
return JSON.parse(text);
|
|
4020
|
+
} catch (error) {
|
|
4021
|
+
throw new UsageError(`${what}: not valid JSON after := (${firstLine(error)}); quote a string as name=value`);
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
function readParamsFile(path2) {
|
|
4025
|
+
const text = readFileOption("--params-file", path2);
|
|
4026
|
+
let parsed;
|
|
4027
|
+
try {
|
|
4028
|
+
parsed = JSON.parse(text);
|
|
4029
|
+
} catch (error) {
|
|
4030
|
+
throw new UsageError(`--params-file ${path2}: not valid JSON (${firstLine(error)})`);
|
|
4031
|
+
}
|
|
4032
|
+
if (!isRecord4(parsed)) {
|
|
4033
|
+
throw new UsageError(`--params-file ${path2}: the parameters are one JSON object of names and values`);
|
|
4034
|
+
}
|
|
4035
|
+
return parsed;
|
|
4036
|
+
}
|
|
4037
|
+
function bareId(value, flag) {
|
|
4038
|
+
if (value.trim() === "") throw new UsageError(`${flag} is empty`);
|
|
4039
|
+
if (value !== value.trim()) throw new UsageError(`${flag} starts or ends with whitespace \u2014 pass the id alone`);
|
|
4040
|
+
return value;
|
|
4041
|
+
}
|
|
4042
|
+
function procedureJson(data) {
|
|
4043
|
+
const procedure = pick2(data, PROCEDURE_FIELDS);
|
|
4044
|
+
if ("versions" in procedure) {
|
|
4045
|
+
procedure["versions"] = records4(procedure["versions"]).map((version2) => pick2(version2, PROCEDURE_VERSION_FIELDS));
|
|
4046
|
+
}
|
|
4047
|
+
return procedure;
|
|
4048
|
+
}
|
|
4049
|
+
function procedureDetail(procedure) {
|
|
4050
|
+
const current = procedure["current_version"];
|
|
4051
|
+
const versions = records4(procedure["versions"]).map(
|
|
4052
|
+
(version2) => ` ${clean(`v${str6(version2["version"])}${version2["version"] === current ? " (current)" : ""} ${str6(version2["inserted_at"])} recipe ${str6(version2["recipe_digest"])}`)}
|
|
4053
|
+
${"Capabilities:".padEnd(15)}${clean(list2(version2["capability_ids"]))}
|
|
4054
|
+
${"Actions:".padEnd(15)}${clean(list2(version2["action_roster"]))}
|
|
4055
|
+
`
|
|
4056
|
+
);
|
|
4057
|
+
return `Procedure ${clean(procedure["name"])} \xB7 v${clean(current)} \xB7 ${clean(procedure["id"])}
|
|
4058
|
+
` + labelled([
|
|
4059
|
+
["Saved from", `job ${str6(procedure["job_id"])}`],
|
|
4060
|
+
["Workbench", str6(procedure["job_url"]) || null],
|
|
4061
|
+
["Created", str6(procedure["inserted_at"]) || null],
|
|
4062
|
+
["Updated", str6(procedure["updated_at"]) || null]
|
|
4063
|
+
]) + (versions.length === 0 ? "\nVersions: none\n" : "\nVersions:\n" + versions.join("")) + parametersSection(procedure["parameter_schema"]) + "\n" + RUN_NOTE;
|
|
4064
|
+
}
|
|
4065
|
+
function parametersSection(schema) {
|
|
4066
|
+
if (!isRecord4(schema) || Object.keys(schema).length === 0) return "\nParameters a run takes: none\n";
|
|
4067
|
+
const lines = JSON.stringify(schema, null, 2).split("\n").map((line) => ` ${clean(line)}
|
|
4068
|
+
`).join("");
|
|
4069
|
+
const scoped = isRecord4(schema["scoped_inputs"]) ? schema["scoped_inputs"]["required"] : void 0;
|
|
4070
|
+
const scopes = Array.isArray(scoped) ? scoped.map(str6) : [];
|
|
4071
|
+
let needs = "";
|
|
4072
|
+
if (scopes.includes("contact_reconciliation")) {
|
|
4073
|
+
needs = "Its runs also need --group <contact-group-id> and --source <uploaded-file-id> (contact_reconciliation).\n";
|
|
4074
|
+
} else if (scopes.length > 0) {
|
|
4075
|
+
needs = `Its runs also need inputs resolved afresh: ${clean(scopes.join(", "))}.
|
|
4076
|
+
`;
|
|
4077
|
+
}
|
|
4078
|
+
return "\nParameters a run takes (JSON schema):\n" + lines + needs;
|
|
4079
|
+
}
|
|
4080
|
+
function list2(value) {
|
|
4081
|
+
return (Array.isArray(value) ? value.map(str6).join(", ") : "") || "none";
|
|
4082
|
+
}
|
|
4083
|
+
function pick2(record, keys) {
|
|
4084
|
+
return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
|
|
4085
|
+
}
|
|
4086
|
+
function records4(value) {
|
|
4087
|
+
return Array.isArray(value) ? value.filter(isRecord4) : [];
|
|
4088
|
+
}
|
|
4089
|
+
function isRecord4(value) {
|
|
4090
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4091
|
+
}
|
|
4092
|
+
function str6(value) {
|
|
4093
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
4094
|
+
}
|
|
4095
|
+
function firstLine(error) {
|
|
4096
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
|
|
4097
|
+
}
|
|
4098
|
+
|
|
2337
4099
|
// src/commands/plans.ts
|
|
2338
4100
|
import { extname as extname2 } from "path";
|
|
2339
4101
|
import { parseDocument } from "yaml";
|
|
2340
4102
|
var PLAN_EXECUTORS = ["none", "operator"];
|
|
2341
4103
|
var CONDITION_KINDS = ["not_before", "gate", "event"];
|
|
4104
|
+
var GATE_OWNER_ROLES = ["gp", "compliance_officer", "valuer", "accountant", "auditor", "counsel"];
|
|
2342
4105
|
function parsePlanDocument(text, source) {
|
|
2343
4106
|
const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
|
|
2344
|
-
if (!
|
|
4107
|
+
if (!isRecord5(parsed)) {
|
|
2345
4108
|
throw new UsageError(`${source}: a plan file holds one object (a mapping), not a list or a scalar`);
|
|
2346
4109
|
}
|
|
2347
4110
|
return parsed;
|
|
@@ -2350,18 +4113,18 @@ function parseJson(text, source) {
|
|
|
2350
4113
|
try {
|
|
2351
4114
|
return JSON.parse(text);
|
|
2352
4115
|
} catch (error) {
|
|
2353
|
-
throw new UsageError(`${source}: not valid JSON (${
|
|
4116
|
+
throw new UsageError(`${source}: not valid JSON (${firstLine2(error)})`);
|
|
2354
4117
|
}
|
|
2355
4118
|
}
|
|
2356
4119
|
function parseYamlDocument(text, source) {
|
|
2357
4120
|
const document = parseDocument(text, { resolveKnownTags: false });
|
|
2358
4121
|
const problem = document.errors[0] ?? document.warnings[0];
|
|
2359
|
-
if (problem !== void 0) throw new UsageError(`${source}: not valid YAML (${
|
|
4122
|
+
if (problem !== void 0) throw new UsageError(`${source}: not valid YAML (${firstLine2(problem)})`);
|
|
2360
4123
|
let value;
|
|
2361
4124
|
try {
|
|
2362
4125
|
value = document.toJS();
|
|
2363
4126
|
} catch (error) {
|
|
2364
|
-
throw new UsageError(`${source}: not valid YAML (${
|
|
4127
|
+
throw new UsageError(`${source}: not valid YAML (${firstLine2(error)})`);
|
|
2365
4128
|
}
|
|
2366
4129
|
if (!jsonSafe(value)) {
|
|
2367
4130
|
throw new UsageError(`${source}: holds a value JSON cannot carry (.inf or .nan) \u2014 write a number or a string`);
|
|
@@ -2374,7 +4137,7 @@ function jsonSafe(value) {
|
|
|
2374
4137
|
if (typeof value === "object" && value !== null) return Object.values(value).every(jsonSafe);
|
|
2375
4138
|
return true;
|
|
2376
4139
|
}
|
|
2377
|
-
function
|
|
4140
|
+
function firstLine2(error) {
|
|
2378
4141
|
return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
|
|
2379
4142
|
}
|
|
2380
4143
|
function readPlanFile(path2, flag = "--file") {
|
|
@@ -2391,7 +4154,7 @@ async function planCreate(ctx, plan, opts) {
|
|
|
2391
4154
|
throw explainMissing(error, `no team ${team} here, or this Harness server has no plan API yet`);
|
|
2392
4155
|
}
|
|
2393
4156
|
const created = body.data;
|
|
2394
|
-
const id =
|
|
4157
|
+
const id = str7(created["id"]);
|
|
2395
4158
|
return {
|
|
2396
4159
|
json: created,
|
|
2397
4160
|
human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
|
|
@@ -2428,7 +4191,7 @@ async function planRelease(ctx, id, opts) {
|
|
|
2428
4191
|
if (opts.spendBudgetCents !== void 0) body["spend_budget_cents"] = opts.spendBudgetCents;
|
|
2429
4192
|
const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/release`, body));
|
|
2430
4193
|
const revision = plan["revision"] ?? {};
|
|
2431
|
-
const ready2 = Array.isArray(plan["steps"]) ? plan["steps"].filter((step) =>
|
|
4194
|
+
const ready2 = Array.isArray(plan["steps"]) ? plan["steps"].filter((step) => isRecord5(step) && step["ready"] === true).length : 0;
|
|
2432
4195
|
return {
|
|
2433
4196
|
json: plan,
|
|
2434
4197
|
human: `Released plan ${clean(id)} as revision ${clean(revision["number"]) || "?"}: ${ready2} ${ready2 === 1 ? "step" : "steps"} ready now
|
|
@@ -2447,23 +4210,22 @@ async function planHold(ctx, id) {
|
|
|
2447
4210
|
async function gateSet(ctx, issue, gate, satisfied) {
|
|
2448
4211
|
requireIdentifiers([issue]);
|
|
2449
4212
|
const wanted = gate.trim();
|
|
2450
|
-
if (wanted === "") throw new UsageError("name the gate by its label or its condition id");
|
|
2451
|
-
|
|
4213
|
+
if (wanted === "") throw new UsageError("name the gate by its key, its label or its condition id");
|
|
4214
|
+
const expert = isExpertToken(ctx.config.token);
|
|
4215
|
+
const list3 = await readConditions(ctx, issue, expert);
|
|
4216
|
+
const target = findGate(list3, issue, wanted, expert);
|
|
4217
|
+
const id = str7(target["id"]);
|
|
4218
|
+
let body;
|
|
2452
4219
|
try {
|
|
2453
|
-
|
|
4220
|
+
body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
|
|
2454
4221
|
} catch (error) {
|
|
2455
|
-
throw
|
|
4222
|
+
throw await explainNotGateOwner(ctx, error, target, issue, satisfied);
|
|
2456
4223
|
}
|
|
2457
|
-
|
|
2458
|
-
const list2 = conditions.filter(isRecord);
|
|
2459
|
-
const target = findGate(list2, issue, wanted);
|
|
2460
|
-
const id = str4(target["id"]);
|
|
2461
|
-
const body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, {
|
|
2462
|
-
satisfied
|
|
2463
|
-
});
|
|
2464
|
-
const others = list2.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length;
|
|
4224
|
+
const others = list3.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length;
|
|
2465
4225
|
let tail = "";
|
|
2466
|
-
if (satisfied) {
|
|
4226
|
+
if (satisfied && expert) {
|
|
4227
|
+
tail = others > 0 ? ` \u2014 ${others} other gate${others === 1 ? "" : "s"} of yours on it ${others === 1 ? "is" : "are"} open` : "";
|
|
4228
|
+
} else if (satisfied) {
|
|
2467
4229
|
tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
|
|
2468
4230
|
}
|
|
2469
4231
|
return {
|
|
@@ -2472,11 +4234,41 @@ async function gateSet(ctx, issue, gate, satisfied) {
|
|
|
2472
4234
|
`
|
|
2473
4235
|
};
|
|
2474
4236
|
}
|
|
4237
|
+
async function conditionList(ctx, issue) {
|
|
4238
|
+
requireIdentifiers([issue]);
|
|
4239
|
+
const expert = isExpertToken(ctx.config.token);
|
|
4240
|
+
const list3 = await readConditions(ctx, issue, expert);
|
|
4241
|
+
let human;
|
|
4242
|
+
if (list3.length === 0) {
|
|
4243
|
+
human = expert ? `You own no gate on ${issue}.
|
|
4244
|
+
` : `No wait conditions on ${issue}.
|
|
4245
|
+
`;
|
|
4246
|
+
} else {
|
|
4247
|
+
human = (expert ? `Your gates on ${issue}:
|
|
4248
|
+
` : `Wait conditions on ${issue}:
|
|
4249
|
+
`) + list3.map((condition) => ` ${conditionLine(condition, { expertView: expert })}
|
|
4250
|
+
`).join("");
|
|
4251
|
+
}
|
|
4252
|
+
return { json: list3, human };
|
|
4253
|
+
}
|
|
4254
|
+
async function readConditions(ctx, issue, expert) {
|
|
4255
|
+
let conditions;
|
|
4256
|
+
try {
|
|
4257
|
+
conditions = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
|
|
4258
|
+
} catch (error) {
|
|
4259
|
+
throw explainMissing(
|
|
4260
|
+
error,
|
|
4261
|
+
expert ? `no step ${issue} assigned to you: a pxc_ credential reads only its own steps` : `no issue ${issue} here, or this Harness server has no wait conditions yet`
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
|
|
4265
|
+
return conditions.filter(isRecord5);
|
|
4266
|
+
}
|
|
2475
4267
|
function requirePlanId(id) {
|
|
2476
4268
|
if (!isUuid(id)) throw new UsageError(`not a plan id: ${id} (expected the plan's UUID)`);
|
|
2477
4269
|
}
|
|
2478
4270
|
function requireDocument(value, what) {
|
|
2479
|
-
if (!
|
|
4271
|
+
if (!isRecord5(value)) throw new UsageError(what);
|
|
2480
4272
|
}
|
|
2481
4273
|
async function planRequest(id, send) {
|
|
2482
4274
|
try {
|
|
@@ -2489,39 +4281,97 @@ function explainMissing(error, why) {
|
|
|
2489
4281
|
if (!(error instanceof HttpError) || error.status !== 404) return error;
|
|
2490
4282
|
return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
|
|
2491
4283
|
}
|
|
2492
|
-
function
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
4284
|
+
async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
4285
|
+
if (!(error instanceof HttpError) || error.status !== 403 || error.body["reason"] !== "not_gate_owner") return error;
|
|
4286
|
+
let current;
|
|
4287
|
+
let currentOwner = error.body["owner"];
|
|
4288
|
+
if (currentOwner !== null && !isRecord5(currentOwner)) {
|
|
4289
|
+
try {
|
|
4290
|
+
const fresh = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
|
|
4291
|
+
if (Array.isArray(fresh)) current = fresh.filter(isRecord5).find((condition) => condition["id"] === gate["id"]);
|
|
4292
|
+
} catch {
|
|
4293
|
+
}
|
|
4294
|
+
currentOwner = current?.["owner"];
|
|
4295
|
+
}
|
|
4296
|
+
const owner = isRecord5(currentOwner) ? ownerText(currentOwner) : "";
|
|
4297
|
+
const subject = `its owner${owner === "" ? "" : `, ${owner},`}`;
|
|
4298
|
+
const named = `gate "${clean((current ?? gate)["label"])}" on ${issue}`;
|
|
4299
|
+
const message = satisfied ? `Cannot satisfy ${named}: only ${subject} can tick it` : `Cannot un-satisfy ${named}: only ${subject} or Harness staff can untick it`;
|
|
4300
|
+
return new HttpError(error.status, message, error.body, error.retryAfter);
|
|
4301
|
+
}
|
|
4302
|
+
var gateLabel = (condition) => str7(condition["label"]).trim();
|
|
4303
|
+
var gateKey = (condition) => str7(condition["key"]);
|
|
4304
|
+
function exactMatches(conditions, wanted) {
|
|
4305
|
+
const id = isUuid(wanted) ? wanted.toLowerCase() : null;
|
|
4306
|
+
return conditions.filter((condition) => {
|
|
4307
|
+
if (id !== null && str7(condition["id"]).toLowerCase() === id) return true;
|
|
4308
|
+
if (condition["kind"] !== "gate") return false;
|
|
4309
|
+
const key = gateKey(condition);
|
|
4310
|
+
return key !== "" && key === (id ?? wanted) || gateLabel(condition) === wanted;
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
function ambiguous(conditions, issue, wanted, matches) {
|
|
4314
|
+
const listed = matches.map((condition) => {
|
|
4315
|
+
const what = condition["kind"] === "gate" ? `gate ${gateName({ label: gateLabel(condition), key: gateKey(condition) })}` : `${clean(condition["kind"])} condition`;
|
|
4316
|
+
return `${clean(condition["id"])} (${what})`;
|
|
4317
|
+
}).join(", ");
|
|
4318
|
+
const unnameable = matches.map((condition) => str7(condition["id"])).filter((id) => exactMatches(conditions, id).length !== 1).map((id) => clean(id));
|
|
4319
|
+
const caveat = unnameable.length === 0 ? "" : ` (${unnameable.join(", ")} ${unnameable.length === 1 ? "is" : "are"} also another gate's key or label, so ${unnameable.length === 1 ? "it" : "they"} cannot be named by id)`;
|
|
4320
|
+
return new CommandError(
|
|
4321
|
+
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean${caveat}`
|
|
4322
|
+
);
|
|
4323
|
+
}
|
|
4324
|
+
function findGate(conditions, issue, wanted, expert = false) {
|
|
4325
|
+
const exact = exactMatches(conditions, wanted);
|
|
4326
|
+
if (exact.length > 1) throw ambiguous(conditions, issue, wanted, exact);
|
|
4327
|
+
if (exact.length === 1) {
|
|
4328
|
+
const only = exact[0];
|
|
4329
|
+
if (only["kind"] !== "gate") {
|
|
2499
4330
|
throw new CommandError(
|
|
2500
|
-
`condition ${wanted} on ${issue} is a ${clean(
|
|
4331
|
+
`condition ${wanted} on ${issue} is a ${clean(only["kind"])} condition \u2014 only a gate is satisfied by hand`
|
|
2501
4332
|
);
|
|
2502
4333
|
}
|
|
2503
|
-
return
|
|
2504
|
-
}
|
|
2505
|
-
let matches = gates.filter((condition) => label(condition) === wanted);
|
|
2506
|
-
if (matches.length === 0) {
|
|
2507
|
-
matches = gates.filter((condition) => label(condition).toLowerCase() === wanted.toLowerCase());
|
|
4334
|
+
return only;
|
|
2508
4335
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
4336
|
+
const gates = conditions.filter((condition) => condition["kind"] === "gate");
|
|
4337
|
+
const folded = wanted.toLowerCase();
|
|
4338
|
+
const loose = gates.filter(
|
|
4339
|
+
(condition) => gateLabel(condition).toLowerCase() === folded || gateKey(condition) !== "" && gateKey(condition) === folded
|
|
4340
|
+
);
|
|
4341
|
+
if (loose.length > 1) throw ambiguous(conditions, issue, wanted, loose);
|
|
4342
|
+
if (loose.length === 1) return loose[0];
|
|
4343
|
+
if (isUuid(wanted)) throw new CommandError(`No condition ${wanted} on ${issue}${expert ? " that you own" : ""}.`);
|
|
4344
|
+
const none = expert ? `No gate of yours on ${issue}` : `No gate on ${issue}`;
|
|
4345
|
+
if (gates.length === 0) {
|
|
4346
|
+
throw new CommandError(`${none} matches "${wanted}" \u2014 ${expert ? "you own no gate on it" : "it has no gates"}`);
|
|
2513
4347
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
throw new CommandError(`No gate on ${issue} matches "${wanted}". Its gates: ${labels}`);
|
|
4348
|
+
const names = gates.map((condition) => gateName({ label: gateLabel(condition), key: gateKey(condition) })).join(", ");
|
|
4349
|
+
throw new CommandError(`${none} matches "${wanted}". ${expert ? "Your" : "Its"} gates: ${names}`);
|
|
2517
4350
|
}
|
|
2518
|
-
function
|
|
4351
|
+
function isRecord5(value) {
|
|
2519
4352
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2520
4353
|
}
|
|
2521
|
-
function
|
|
4354
|
+
function str7(value) {
|
|
2522
4355
|
return value === null || value === void 0 ? "" : String(value);
|
|
2523
4356
|
}
|
|
2524
4357
|
|
|
4358
|
+
// src/commands/expert.ts
|
|
4359
|
+
async function assignments(ctx) {
|
|
4360
|
+
const data = (await ctx.client.get(EXPERT_ASSIGNMENTS_PATH)).data;
|
|
4361
|
+
if (!isRecord6(data) || !Array.isArray(data["assignments"])) {
|
|
4362
|
+
throw new CommandError("The assignments response carried no list.");
|
|
4363
|
+
}
|
|
4364
|
+
const steps = data["assignments"].filter(isRecord6);
|
|
4365
|
+
const expert = isRecord6(data["expert"]) ? data["expert"] : {};
|
|
4366
|
+
const plan = isRecord6(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
|
|
4367
|
+
const header = expertSummary({ role: expert["role"], plan, assigned_steps: steps.length });
|
|
4368
|
+
return { json: data, human: `${header}
|
|
4369
|
+
${expertAssignments(steps)}` };
|
|
4370
|
+
}
|
|
4371
|
+
function isRecord6(value) {
|
|
4372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4373
|
+
}
|
|
4374
|
+
|
|
2525
4375
|
// src/core/credentials.ts
|
|
2526
4376
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2527
4377
|
import { homedir } from "os";
|
|
@@ -2590,13 +4440,13 @@ function readRepoConfig(path2) {
|
|
|
2590
4440
|
const record = parsed;
|
|
2591
4441
|
const config = { path: path2 };
|
|
2592
4442
|
const warnings = [];
|
|
2593
|
-
for (const
|
|
2594
|
-
const value = record[
|
|
4443
|
+
for (const field3 of ["team", "url", "org"]) {
|
|
4444
|
+
const value = record[field3];
|
|
2595
4445
|
if (value === void 0) continue;
|
|
2596
4446
|
if (typeof value === "string" && value.trim() !== "") {
|
|
2597
|
-
config[
|
|
4447
|
+
config[field3] = value.trim();
|
|
2598
4448
|
} else {
|
|
2599
|
-
warnings.push(`harness: ignoring "${
|
|
4449
|
+
warnings.push(`harness: ignoring "${field3}" in ${path2} \u2014 expected a non-empty string`);
|
|
2600
4450
|
}
|
|
2601
4451
|
}
|
|
2602
4452
|
return { config, warnings };
|
|
@@ -2753,6 +4603,9 @@ harness:read scope \u2014 plus harness:write only where the workflow
|
|
|
2753
4603
|
creates or closes \u2014 set an expiry, and export it:
|
|
2754
4604
|
|
|
2755
4605
|
export HARNESS_API_TOKEN=aone_...
|
|
4606
|
+
|
|
4607
|
+
An outside expert uses the pxc_ credential minted in their expert view:
|
|
4608
|
+
export it the same way, or paste it into \`harness login --no-browser\`.
|
|
2756
4609
|
`;
|
|
2757
4610
|
}
|
|
2758
4611
|
function blankToNull(value) {
|
|
@@ -2768,9 +4621,15 @@ export {
|
|
|
2768
4621
|
LocalError,
|
|
2769
4622
|
CommandError,
|
|
2770
4623
|
CLI_VERSION,
|
|
4624
|
+
CLI_PACKAGE,
|
|
4625
|
+
EXPERT_ASSIGNMENTS_PATH,
|
|
4626
|
+
isExpertToken,
|
|
4627
|
+
EXPERT_LAPSED,
|
|
4628
|
+
explainExpertRefusal,
|
|
2771
4629
|
Client,
|
|
2772
4630
|
clean,
|
|
2773
4631
|
json,
|
|
4632
|
+
expertSummary,
|
|
2774
4633
|
errorDetails,
|
|
2775
4634
|
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
2776
4635
|
IDEMPOTENCY_KEY_FORMAT,
|
|
@@ -2820,7 +4679,7 @@ export {
|
|
|
2820
4679
|
linkAdd,
|
|
2821
4680
|
linkUpdate,
|
|
2822
4681
|
linkRm,
|
|
2823
|
-
labelList,
|
|
4682
|
+
labelList2 as labelList,
|
|
2824
4683
|
labelCreate,
|
|
2825
4684
|
teamCreate,
|
|
2826
4685
|
teamList,
|
|
@@ -2838,9 +4697,45 @@ export {
|
|
|
2838
4697
|
memberAdd,
|
|
2839
4698
|
memberSetRole,
|
|
2840
4699
|
upload,
|
|
4700
|
+
toolsList,
|
|
4701
|
+
toolsSearch,
|
|
4702
|
+
toolsDomains,
|
|
4703
|
+
toolShow,
|
|
4704
|
+
skillsList,
|
|
4705
|
+
requireCapabilityId,
|
|
4706
|
+
requireSearchQuery,
|
|
4707
|
+
requireToolsOpts,
|
|
4708
|
+
requireSchemaView,
|
|
4709
|
+
JOB_STATES,
|
|
4710
|
+
JOB_STATUS_ALIASES,
|
|
4711
|
+
JOBS_MAX_LIMIT,
|
|
4712
|
+
JOBS_MAX_PAGES,
|
|
4713
|
+
EXIT_WAITING_FOR_PERSON,
|
|
4714
|
+
EXIT_JOB_ENDED,
|
|
4715
|
+
EXIT_INTERRUPTED,
|
|
4716
|
+
requireJobsArgs,
|
|
4717
|
+
jobsList,
|
|
4718
|
+
requireJobId,
|
|
4719
|
+
jobShow,
|
|
4720
|
+
jobWatch,
|
|
4721
|
+
watchStopSignal,
|
|
4722
|
+
jobContinue,
|
|
2841
4723
|
resolveTextSources,
|
|
4724
|
+
STEER_MAX_CHARACTERS,
|
|
4725
|
+
instructionFrom,
|
|
4726
|
+
jobSteer,
|
|
4727
|
+
jobCancel,
|
|
4728
|
+
jobPause,
|
|
4729
|
+
requireProcedureId,
|
|
4730
|
+
requireProcedureName,
|
|
4731
|
+
procedureSave,
|
|
4732
|
+
procedureShow,
|
|
4733
|
+
prepareProcedureRun,
|
|
4734
|
+
startProcedureRun,
|
|
4735
|
+
procedureRun,
|
|
2842
4736
|
PLAN_EXECUTORS,
|
|
2843
4737
|
CONDITION_KINDS,
|
|
4738
|
+
GATE_OWNER_ROLES,
|
|
2844
4739
|
readPlanFile,
|
|
2845
4740
|
planCreate,
|
|
2846
4741
|
planShow,
|
|
@@ -2848,7 +4743,9 @@ export {
|
|
|
2848
4743
|
planRelease,
|
|
2849
4744
|
planHold,
|
|
2850
4745
|
gateSet,
|
|
4746
|
+
conditionList,
|
|
2851
4747
|
requirePlanId,
|
|
4748
|
+
assignments,
|
|
2852
4749
|
defaultCredentialsPath,
|
|
2853
4750
|
readCredentials,
|
|
2854
4751
|
writeCredentials,
|