@allocator-one/harness-cli 1.15.0 → 1.16.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 +192 -47
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-I2S2JVCQ.js → chunk-GVTZKAXC.js} +432 -107
- package/dist/cli.js +455 -158
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-O7CX2ZCC.js → server-F6YRKNC2.js} +78 -17
- package/package.json +1 -1
- package/spec/openapi.json +6593 -4421
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CLI_PACKAGE,
|
|
4
|
+
CLI_VERSION,
|
|
5
|
+
invokedName,
|
|
6
|
+
named
|
|
7
|
+
} from "./chunk-FQWV5MIC.js";
|
|
2
8
|
|
|
3
9
|
// src/core/errors.ts
|
|
4
10
|
var HttpError = class extends Error {
|
|
@@ -52,14 +58,6 @@ var CommandError = class extends Error {
|
|
|
52
58
|
}
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
// src/version.ts
|
|
56
|
-
import { readFileSync } from "fs";
|
|
57
|
-
var pkg = JSON.parse(
|
|
58
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
59
|
-
);
|
|
60
|
-
var CLI_VERSION = pkg.version;
|
|
61
|
-
var CLI_PACKAGE = pkg.name;
|
|
62
|
-
|
|
63
61
|
// src/core/expert.ts
|
|
64
62
|
var EXPERT_TOKEN_PREFIX = "pxc_";
|
|
65
63
|
var EXPERT_ASSIGNMENTS_PATH = "/api/v1/expert/assignments";
|
|
@@ -93,9 +91,15 @@ var DEFAULT_MESSAGES = {
|
|
|
93
91
|
404: "Not found",
|
|
94
92
|
409: "Conflict",
|
|
95
93
|
422: "Validation error",
|
|
96
|
-
426:
|
|
94
|
+
426: `CLI too old for this API \u2014 upgrade: npm i -g ${CLI_PACKAGE}@latest`,
|
|
97
95
|
429: "Rate limited \u2014 retry later"
|
|
98
96
|
};
|
|
97
|
+
var API_VERSION_HEADER = "x-harness-api-version";
|
|
98
|
+
var STATS_PATH = "/api/v1/stats";
|
|
99
|
+
var SESSION_PATH = "/api/v1/session";
|
|
100
|
+
function isLoginCredential(token) {
|
|
101
|
+
return !token.startsWith("aone_") && !isExpertToken(token);
|
|
102
|
+
}
|
|
99
103
|
var Client = class {
|
|
100
104
|
constructor(config) {
|
|
101
105
|
this.config = config;
|
|
@@ -153,16 +157,32 @@ var Client = class {
|
|
|
153
157
|
throw new TransportError(transportMessage(error), transportCode(error));
|
|
154
158
|
}
|
|
155
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A GET that also hands back the server's API version header, so a probe
|
|
162
|
+
* that verifies a credential reports the version without a second
|
|
163
|
+
* request. Errors map as for `get`.
|
|
164
|
+
*/
|
|
165
|
+
async getVersioned(path2) {
|
|
166
|
+
const response = await this.raw("GET", path2, {});
|
|
167
|
+
const apiVersion = response.headers.get(API_VERSION_HEADER);
|
|
168
|
+
return { envelope: await this.json(response), apiVersion };
|
|
169
|
+
}
|
|
156
170
|
/**
|
|
157
171
|
* The server's API version, for `version`'s server side. The header rides
|
|
158
|
-
* on every response
|
|
159
|
-
* 401 answers.
|
|
160
|
-
* probe is never a refusal in the server's log
|
|
172
|
+
* on every response of a route the server has — the versioning plug runs
|
|
173
|
+
* before auth — so even a 401 answers. Each credential probes a route it
|
|
174
|
+
* reaches, so the probe is never a refusal in the server's log: a pxc_
|
|
175
|
+
* credential its assignments, a login credential of either kind (the
|
|
176
|
+
* Operator-only one reaches no tracker read) GET /api/v1/session, and an
|
|
177
|
+
* aone_ token the stats. A server older than GET /api/v1/session answers
|
|
178
|
+
* that route's 404 without the header, so the stats answer then.
|
|
161
179
|
*/
|
|
162
180
|
async serverVersion() {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
181
|
+
const token = this.config.token;
|
|
182
|
+
const path2 = isExpertToken(token) ? EXPERT_ASSIGNMENTS_PATH : isLoginCredential(token) ? SESSION_PATH : STATS_PATH;
|
|
183
|
+
const version2 = (await this.raw("GET", path2, {})).headers.get(API_VERSION_HEADER);
|
|
184
|
+
if (version2 !== null || path2 !== SESSION_PATH) return version2 ?? "unknown";
|
|
185
|
+
return (await this.raw("GET", STATS_PATH, {})).headers.get(API_VERSION_HEADER) ?? "unknown";
|
|
166
186
|
}
|
|
167
187
|
// Only an explicitly configured actor is sent, and never over one the
|
|
168
188
|
// caller already put in the body.
|
|
@@ -311,6 +331,7 @@ function issueDetail(issue, images = []) {
|
|
|
311
331
|
["Priority", issue["priority_label"]],
|
|
312
332
|
["Assignee", path(issue, "assignee", "name")],
|
|
313
333
|
["Team", path(issue, "team", "key")],
|
|
334
|
+
["Plan", issuePlanLine(issue["plan"])],
|
|
314
335
|
["Labels", labelsLine(issue["labels"])],
|
|
315
336
|
["Blocked", blockedLine(issue)],
|
|
316
337
|
["Waiting", waitingLine(issue)],
|
|
@@ -715,6 +736,34 @@ function roleName(role) {
|
|
|
715
736
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
716
737
|
}
|
|
717
738
|
var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
|
|
739
|
+
function plansTable(plans) {
|
|
740
|
+
const rows = plans.map((plan) => [
|
|
741
|
+
str(plan["root"]) || "-",
|
|
742
|
+
str(plan["status"]) || "-",
|
|
743
|
+
plan["held"] === true ? "yes" : plan["held"] === false ? "no" : "-",
|
|
744
|
+
typeof plan["revision"] === "number" ? String(plan["revision"]) : "-",
|
|
745
|
+
truncate(plan["title"])
|
|
746
|
+
]);
|
|
747
|
+
return table(["ROOT", "STATUS", "HELD", "REVISION", "TITLE"], rows);
|
|
748
|
+
}
|
|
749
|
+
function issuePlanLine(plan) {
|
|
750
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return null;
|
|
751
|
+
const record = plan;
|
|
752
|
+
const root = clean(record["root"]);
|
|
753
|
+
if (root === "") return null;
|
|
754
|
+
const status = record["held"] === true ? "held" : clean(record["status"]);
|
|
755
|
+
const revision = record["revision"];
|
|
756
|
+
const parts = [root];
|
|
757
|
+
if (status !== "") parts.push(status);
|
|
758
|
+
if (typeof revision === "number") {
|
|
759
|
+
parts.push(`revision ${revision}`);
|
|
760
|
+
if (typeof record["in_revision"] === "boolean") parts.push(record["in_revision"] ? "in revision" : "not in revision");
|
|
761
|
+
} else {
|
|
762
|
+
parts.push("never released");
|
|
763
|
+
}
|
|
764
|
+
if (record["executor"] === "operator") parts.push("Operator step");
|
|
765
|
+
return parts.join(" \xB7 ");
|
|
766
|
+
}
|
|
718
767
|
function planDetail(plan) {
|
|
719
768
|
const steps = records(plan["steps"]);
|
|
720
769
|
const fields = [
|
|
@@ -843,7 +892,7 @@ function holderText(holder) {
|
|
|
843
892
|
function planHeldSection(heldSteps) {
|
|
844
893
|
if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
|
|
845
894
|
return `
|
|
846
|
-
|
|
895
|
+
Changed since the release: ${heldSteps.map((step) => clean(step)).join(", ")} (only Operator steps are held until the next release takes them in)
|
|
847
896
|
`;
|
|
848
897
|
}
|
|
849
898
|
function indentLines(text) {
|
|
@@ -863,11 +912,32 @@ function changesetDetails(details) {
|
|
|
863
912
|
function violationDetails(violations) {
|
|
864
913
|
return records(violations).map((violation) => {
|
|
865
914
|
const code = clean(violation["code"]);
|
|
866
|
-
const message = clean(violation["message"]) || code || "invalid";
|
|
915
|
+
const message = capMessage(violation) ?? (clean(violation["message"]) || code || "invalid");
|
|
867
916
|
return `
|
|
868
917
|
${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
|
|
869
918
|
}).join("");
|
|
870
919
|
}
|
|
920
|
+
var MAX_PLAN_STEPS_CEILING = 2e3;
|
|
921
|
+
function capMessage(violation) {
|
|
922
|
+
if (violation["code"] !== "too_many_steps") return null;
|
|
923
|
+
const cap = violation["cap"];
|
|
924
|
+
if (typeof cap !== "object" || cap === null || Array.isArray(cap)) return null;
|
|
925
|
+
const { kind, max, count, ceiling: served } = cap;
|
|
926
|
+
if (!Number.isInteger(max) || !Number.isInteger(count)) return null;
|
|
927
|
+
if (kind === "operator_steps") {
|
|
928
|
+
return `the plan has ${count} Operator steps, above the fixed limit of ${max}: Operator's delegated grant caps a plan's agent steps, and no setting raises it \u2014 hand some to people (executor none) or split the plan`;
|
|
929
|
+
}
|
|
930
|
+
if (kind === "plan_steps") {
|
|
931
|
+
const ceiling = Number.isInteger(served) ? served : MAX_PLAN_STEPS_CEILING;
|
|
932
|
+
const over = `the plan has ${count} steps, above this organization's max plan steps (${max})`;
|
|
933
|
+
if (count > ceiling && max < ceiling) {
|
|
934
|
+
return `${over}, and above the ceiling of ${ceiling} that no setting raises past \u2014 split the plan`;
|
|
935
|
+
}
|
|
936
|
+
if (max >= ceiling) return `${over}, which is already at its ceiling of ${ceiling} \u2014 split the plan`;
|
|
937
|
+
return `${over}: a Harness admin can raise "Max plan steps" in Harness settings \u2192 Plans (max_plan_steps, up to ${ceiling})`;
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
871
941
|
function violationSubject(violation) {
|
|
872
942
|
const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
|
|
873
943
|
const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
|
|
@@ -966,7 +1036,7 @@ function path(record, ...keys) {
|
|
|
966
1036
|
}
|
|
967
1037
|
|
|
968
1038
|
// src/commands/issues.ts
|
|
969
|
-
import { openAsBlob, openSync, closeSync, readFileSync
|
|
1039
|
+
import { openAsBlob, openSync, closeSync, readFileSync, statSync as statSync2 } from "fs";
|
|
970
1040
|
import { basename, extname } from "path";
|
|
971
1041
|
|
|
972
1042
|
// src/core/attachments.ts
|
|
@@ -1165,6 +1235,9 @@ var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
|
|
|
1165
1235
|
function isUuid(value) {
|
|
1166
1236
|
return UUID_FORMAT.test(value);
|
|
1167
1237
|
}
|
|
1238
|
+
function isIdentifier(value) {
|
|
1239
|
+
return IDENTIFIER_FORMAT.test(value);
|
|
1240
|
+
}
|
|
1168
1241
|
function requireIdentifiers(identifiers) {
|
|
1169
1242
|
const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
|
|
1170
1243
|
if (bad.length > 0) {
|
|
@@ -1198,7 +1271,7 @@ function requireReadyClaimArgs(opts) {
|
|
|
1198
1271
|
async function claimNext(ctx, opts) {
|
|
1199
1272
|
requireReadyClaimArgs(opts);
|
|
1200
1273
|
const team = requiredTeam(ctx, opts.team);
|
|
1201
|
-
const check = `check
|
|
1274
|
+
const check = `check \`${invokedName()} list --team ${team} --assignee me --status in_progress\``;
|
|
1202
1275
|
const unreadable = (why) => new CommandError(`The claim-next response ${why}, so an issue may have been claimed \u2014 ${check}.`);
|
|
1203
1276
|
let body;
|
|
1204
1277
|
try {
|
|
@@ -1356,14 +1429,14 @@ async function prime(ctx) {
|
|
|
1356
1429
|
const statsBody = await ctx.client.get("/api/v1/stats");
|
|
1357
1430
|
const ready2 = markAgentSteps(await mostImportantReady(ctx));
|
|
1358
1431
|
const json2 = { stats: statsBody.data, ready: ready2 };
|
|
1359
|
-
const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + `
|
|
1432
|
+
const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + named(`
|
|
1360
1433
|
Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
|
|
1361
1434
|
harness close <id> --reason "Implemented in <commit-hash>" (response
|
|
1362
1435
|
lists newly unblocked issues \u2014 surface them).
|
|
1363
1436
|
Create tracked work BEFORE coding: harness create --title "\u2026" --type task.
|
|
1364
1437
|
Dependencies: harness dep add <issue> <blocker> (blocker blocks issue).
|
|
1365
1438
|
Before reporting done: close the issues you finished.
|
|
1366
|
-
|
|
1439
|
+
`);
|
|
1367
1440
|
return { json: json2, human };
|
|
1368
1441
|
}
|
|
1369
1442
|
async function mostImportantReady(ctx) {
|
|
@@ -1381,7 +1454,7 @@ async function version(config) {
|
|
|
1381
1454
|
} else {
|
|
1382
1455
|
server = await new Client(config).serverVersion().catch(() => "unreachable");
|
|
1383
1456
|
}
|
|
1384
|
-
return { json: { cli: CLI_VERSION, server }, human:
|
|
1457
|
+
return { json: { cli: CLI_VERSION, server }, human: `${invokedName()} ${CLI_VERSION} (server API ${server})
|
|
1385
1458
|
` };
|
|
1386
1459
|
}
|
|
1387
1460
|
async function create(ctx, opts) {
|
|
@@ -1494,18 +1567,42 @@ ${opts.appendNotes}`;
|
|
|
1494
1567
|
return { json: issue, human };
|
|
1495
1568
|
}
|
|
1496
1569
|
var CLAIM_REFUSAL_HINTS = {
|
|
1497
|
-
//
|
|
1498
|
-
// the step is
|
|
1499
|
-
|
|
1500
|
-
|
|
1570
|
+
// A person never claims an Operator step, so releasing the plan is not the
|
|
1571
|
+
// fix: handing the step to a person is, and a handed-over Operator step
|
|
1572
|
+
// stays frozen until the next release.
|
|
1573
|
+
agent_step: (_identifier, plan) => {
|
|
1574
|
+
const runs = plan.kind === "sole" ? `plan ${plan.root}` : plan.kind === "nearest" ? `its plans (several govern it; the nearest is ${plan.root})` : "its plan";
|
|
1575
|
+
const target = plan.kind === "sole" ? plan.root : "<plan>";
|
|
1576
|
+
return `a person can't claim an Operator step: Operator runs it for ${runs}. To do it yourself, set its executor to none (${invokedName()} plan edit ${target}) and release the plan again`;
|
|
1577
|
+
},
|
|
1578
|
+
plan_not_released: (_identifier, plan) => planReleaseHint(plan),
|
|
1579
|
+
not_in_plan_revision: (_identifier, plan) => planReleaseHint(plan),
|
|
1580
|
+
edited_since_release: (_identifier, plan) => planReleaseHint(plan),
|
|
1581
|
+
waiting_on_condition: (identifier) => `${invokedName()} show ${identifier} lists what it waits on; tick a gate with ${invokedName()} gate satisfy ${identifier} <gate>`
|
|
1501
1582
|
};
|
|
1583
|
+
function planReleaseHint(plan) {
|
|
1584
|
+
if (plan.kind === "sole") return plan.release ? `run ${invokedName()} plan release ${plan.root}` : void 0;
|
|
1585
|
+
if (plan.kind === "nearest") {
|
|
1586
|
+
return `several plans govern it; the nearest is ${plan.root} (${invokedName()} plan show ${plan.root})`;
|
|
1587
|
+
}
|
|
1588
|
+
return void 0;
|
|
1589
|
+
}
|
|
1502
1590
|
function explainClaimRefusal(error, identifier) {
|
|
1503
1591
|
if (!(error instanceof HttpError) || error.status !== 409) return error;
|
|
1504
1592
|
const reason = error.body["reason"];
|
|
1505
|
-
const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier) : void 0;
|
|
1593
|
+
const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier, refusingPlan(error.body)) : void 0;
|
|
1506
1594
|
if (hint === void 0) return error;
|
|
1507
1595
|
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
1508
1596
|
}
|
|
1597
|
+
function refusingPlan(body) {
|
|
1598
|
+
const plan = body["plan"];
|
|
1599
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return { kind: "none" };
|
|
1600
|
+
const root = plan["root"];
|
|
1601
|
+
if (typeof root !== "string" || !isIdentifier(root)) return { kind: "none" };
|
|
1602
|
+
const message = typeof body["message"] === "string" ? body["message"] : "";
|
|
1603
|
+
if (!new RegExp(`\\bplan ${root}\\b`).test(message)) return { kind: "nearest", root };
|
|
1604
|
+
return { kind: "sole", root, release: new RegExp(`\\brelease plan ${root}\\b`).test(message) };
|
|
1605
|
+
}
|
|
1509
1606
|
function requireCloseArgs(identifiers, opts) {
|
|
1510
1607
|
if (identifiers.length === 0) {
|
|
1511
1608
|
throw new UsageError(
|
|
@@ -1604,9 +1701,9 @@ async function archive(ctx, identifiers, opts) {
|
|
|
1604
1701
|
}
|
|
1605
1702
|
async function comments(ctx, identifier) {
|
|
1606
1703
|
requireIdentifiers([identifier]);
|
|
1607
|
-
const
|
|
1608
|
-
if (
|
|
1609
|
-
const human =
|
|
1704
|
+
const records5 = await completeCommentThread(ctx, identifier);
|
|
1705
|
+
if (records5.length === 0) return { json: records5, human: "No comments.\n" };
|
|
1706
|
+
const human = records5.map((comment2) => {
|
|
1610
1707
|
const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
|
|
1611
1708
|
const id = clean(comment2["id"] ?? "unknown id");
|
|
1612
1709
|
const text = cleanMultiline(str2(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
|
|
@@ -1614,7 +1711,35 @@ async function comments(ctx, identifier) {
|
|
|
1614
1711
|
${text}
|
|
1615
1712
|
`;
|
|
1616
1713
|
}).join("\n");
|
|
1617
|
-
return { json:
|
|
1714
|
+
return { json: records5, human };
|
|
1715
|
+
}
|
|
1716
|
+
async function completeCommentThread(ctx, identifier) {
|
|
1717
|
+
const pages = [];
|
|
1718
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1719
|
+
let before;
|
|
1720
|
+
for (; ; ) {
|
|
1721
|
+
const page = await ctx.client.get(`/api/v1/issues/${identifier}/comments`, {
|
|
1722
|
+
limit: 200,
|
|
1723
|
+
before
|
|
1724
|
+
});
|
|
1725
|
+
if (!Array.isArray(page.data) || typeof page.meta?.has_more !== "boolean") {
|
|
1726
|
+
throw new CommandError("Invalid comment pagination: expected a comment array and boolean meta.has_more; completeness unknown.");
|
|
1727
|
+
}
|
|
1728
|
+
const records5 = [];
|
|
1729
|
+
for (const record of page.data) {
|
|
1730
|
+
if (record === null || typeof record !== "object" || Array.isArray(record) || typeof record.id !== "string" || record.id.trim() === "" || seen.has(record.id)) {
|
|
1731
|
+
throw new CommandError("Invalid comment pagination: missing or repeated comment id; completeness unknown.");
|
|
1732
|
+
}
|
|
1733
|
+
seen.add(record.id);
|
|
1734
|
+
records5.push(record);
|
|
1735
|
+
}
|
|
1736
|
+
pages.push(records5);
|
|
1737
|
+
if (!page.meta.has_more) return pages.reverse().flat();
|
|
1738
|
+
if (records5.length === 0) {
|
|
1739
|
+
throw new CommandError("Invalid comment pagination: has_more on an empty page; completeness unknown.");
|
|
1740
|
+
}
|
|
1741
|
+
before = records5[0]["id"];
|
|
1742
|
+
}
|
|
1618
1743
|
}
|
|
1619
1744
|
async function dep(ctx, verb, issue, blocker) {
|
|
1620
1745
|
requireIdentifiers([issue, blocker]);
|
|
@@ -1764,7 +1889,7 @@ async function attachFile(ctx, path2) {
|
|
|
1764
1889
|
}
|
|
1765
1890
|
async function uploadAttachment(ctx, path2, maxBytes) {
|
|
1766
1891
|
attachableFile(path2, maxBytes);
|
|
1767
|
-
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([
|
|
1892
|
+
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([readFileSync(path2)], basename(path2));
|
|
1768
1893
|
requireAttachmentSize(path2, file.size, maxBytes);
|
|
1769
1894
|
const form = new FormData();
|
|
1770
1895
|
form.set("file", file);
|
|
@@ -1871,13 +1996,13 @@ async function prepareApplicableLabelIds(ctx, teamId, names) {
|
|
|
1871
1996
|
}
|
|
1872
1997
|
function applicableLabel(labels, input, teamId) {
|
|
1873
1998
|
const normalized = normalizeLabelName(input);
|
|
1874
|
-
const
|
|
1875
|
-
const teamMatches =
|
|
1999
|
+
const named2 = labels.filter((label) => normalizeLabelName(label["name"]) === normalized);
|
|
2000
|
+
const teamMatches = named2.filter((label) => label["team_id"] === teamId);
|
|
1876
2001
|
if (teamMatches.length > 1) {
|
|
1877
2002
|
throw new CommandError(`more than one team label matches "${input}"; use a label id`);
|
|
1878
2003
|
}
|
|
1879
2004
|
if (teamMatches.length === 1) return teamMatches[0];
|
|
1880
|
-
const workspaceMatches =
|
|
2005
|
+
const workspaceMatches = named2.filter(
|
|
1881
2006
|
(label) => label["team_id"] === null || label["team_id"] === void 0
|
|
1882
2007
|
);
|
|
1883
2008
|
if (workspaceMatches.length > 1) {
|
|
@@ -2003,8 +2128,8 @@ function listParams(ctx, opts) {
|
|
|
2003
2128
|
}
|
|
2004
2129
|
function optionalPriority(value) {
|
|
2005
2130
|
if (value === void 0) return null;
|
|
2006
|
-
const
|
|
2007
|
-
if (
|
|
2131
|
+
const named2 = Object.hasOwn(PRIORITY_NAMES, value) ? PRIORITY_NAMES[value] : void 0;
|
|
2132
|
+
if (named2 !== void 0) return named2;
|
|
2008
2133
|
if (/^\d+$/.test(value)) {
|
|
2009
2134
|
throw new UsageError(
|
|
2010
2135
|
`Bare numeric priorities are refused: bd and harness disagree on what
|
|
@@ -2518,7 +2643,7 @@ var APPROVAL_NOTE = "Write and send capabilities, and any proposal, need a perso
|
|
|
2518
2643
|
var REFUSALS = {
|
|
2519
2644
|
operator_disabled: "nothing here works until it is switched back on",
|
|
2520
2645
|
operator_not_permitted: "this account has no admin or member role in the organization the credential is bound to",
|
|
2521
|
-
actor_required:
|
|
2646
|
+
actor_required: `this credential acts as no person: sign in with \`${invokedName()} login\`, or use an API token minted for a user`,
|
|
2522
2647
|
run_bound_credential: "a run-bound credential can't read Operator's catalog",
|
|
2523
2648
|
forbidden: "Operator needs the operator:read scope (aone_ API tokens must be minted with it) and an admin or member role"
|
|
2524
2649
|
};
|
|
@@ -2638,7 +2763,7 @@ function requireCapabilityId(id) {
|
|
|
2638
2763
|
throw new UsageError(`not a capability id: ${clean(id)} (e.g. crm.party.search)`);
|
|
2639
2764
|
}
|
|
2640
2765
|
if (id === "domains") {
|
|
2641
|
-
throw new UsageError(
|
|
2766
|
+
throw new UsageError(`domains is not a capability id \u2014 \`${invokedName()} tools domains\` lists the domains`);
|
|
2642
2767
|
}
|
|
2643
2768
|
}
|
|
2644
2769
|
function requireSearchQuery(query) {
|
|
@@ -2995,9 +3120,9 @@ function jobPeople(job) {
|
|
|
2995
3120
|
const participants = records3(field2(job, "people", "participants"));
|
|
2996
3121
|
const owner = isRecord2(job["owner"]) ? job["owner"] : null;
|
|
2997
3122
|
const names = /* @__PURE__ */ new Map();
|
|
2998
|
-
for (const
|
|
2999
|
-
if (typeof
|
|
3000
|
-
if (!names.has(
|
|
3123
|
+
for (const named2 of owner === null ? participants : [...participants, owner]) {
|
|
3124
|
+
if (typeof named2["user_id"] === "string" && typeof named2["name"] === "string" && named2["name"] !== "") {
|
|
3125
|
+
if (!names.has(named2["user_id"])) names.set(named2["user_id"], named2["name"]);
|
|
3001
3126
|
}
|
|
3002
3127
|
}
|
|
3003
3128
|
const person = (userId) => typeof userId === "string" && userId !== "" ? { user_id: userId, name: names.get(userId) ?? null } : null;
|
|
@@ -3090,7 +3215,8 @@ async function jobWatch(ctx, id, opts) {
|
|
|
3090
3215
|
}
|
|
3091
3216
|
}
|
|
3092
3217
|
function resumeLine(id, after) {
|
|
3093
|
-
|
|
3218
|
+
const name = invokedName();
|
|
3219
|
+
return `${name}: stopped watching job ${id}; resume with: ${name} watch ${id} --after ${after}
|
|
3094
3220
|
`;
|
|
3095
3221
|
}
|
|
3096
3222
|
function watchStopSignal(stdout = process.stdout, signals = process) {
|
|
@@ -3179,9 +3305,9 @@ async function eventsPage(ctx, id, after) {
|
|
|
3179
3305
|
var REFUSAL_HINTS = {
|
|
3180
3306
|
operator_disabled: "nothing can be read until Operator is switched back on for this organization",
|
|
3181
3307
|
operator_not_permitted: "only the organization's admins and members use Operator; guests and people outside it cannot",
|
|
3182
|
-
actor_required:
|
|
3183
|
-
forbidden:
|
|
3184
|
-
run_bound_credential:
|
|
3308
|
+
actor_required: `the credential acts as no person: sign in with \`${invokedName()} login\`, or use an API token a person created`,
|
|
3309
|
+
forbidden: `the credential lacks the operator:read scope (\`${invokedName()} login\` carries it; an aone_ token must be created with it, bound to this organization), or you are a guest or outside the organization`,
|
|
3310
|
+
run_bound_credential: `a run-bound credential cannot read Operator jobs; sign in with \`${invokedName()} login\``
|
|
3185
3311
|
};
|
|
3186
3312
|
function missingJob(id) {
|
|
3187
3313
|
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)`;
|
|
@@ -3567,10 +3693,10 @@ async function sendOnce(ctx, path2, body, write2) {
|
|
|
3567
3693
|
throw error;
|
|
3568
3694
|
}
|
|
3569
3695
|
}
|
|
3570
|
-
function explainRefusal(error, notFound2,
|
|
3696
|
+
function explainRefusal(error, notFound2, hints2) {
|
|
3571
3697
|
if (!(error instanceof HttpError) || error instanceof UpgradeRequiredError) return error;
|
|
3572
3698
|
const code = typeof error.body["error"] === "string" ? error.body["error"] : "";
|
|
3573
|
-
const own = Object.hasOwn(
|
|
3699
|
+
const own = Object.hasOwn(hints2, code) ? hints2[code] : void 0;
|
|
3574
3700
|
let hint;
|
|
3575
3701
|
if (error.status >= 500) hint = own;
|
|
3576
3702
|
else if (error.status === 404) hint = notFound2;
|
|
@@ -3578,10 +3704,132 @@ function explainRefusal(error, notFound2, hints) {
|
|
|
3578
3704
|
else if (error.status === 403 && Object.hasOwn(ACCESS_HINTS, code)) hint = ACCESS_HINTS[code];
|
|
3579
3705
|
else if (error.status === 400 && code === "bad_request") hint = BAD_REQUEST_HINT;
|
|
3580
3706
|
if (hint === void 0) return error;
|
|
3581
|
-
const message = error.status
|
|
3707
|
+
const message = error.status < 500 ? error.message : typeof error.body["message"] === "string" ? error.body["message"] : own !== void 0 ? `The server answered ${error.status}` : error.message;
|
|
3582
3708
|
return new HttpError(error.status, `${message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
3583
3709
|
}
|
|
3584
3710
|
|
|
3711
|
+
// src/commands/operator-run.ts
|
|
3712
|
+
var OUTCOME_MAX_CHARACTERS = 1e4;
|
|
3713
|
+
var BUDGET_MAX_DOLLARS = 1e6;
|
|
3714
|
+
var SKILL = /^\/?[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3715
|
+
var SKILL_MAX_CHARACTERS = 65;
|
|
3716
|
+
var BUDGET = /^\d+(\.\d{1,2})?$/;
|
|
3717
|
+
function prepareRun(outcome, opts = {}, surface = CLI_SURFACE) {
|
|
3718
|
+
const trimmed = outcome.trim();
|
|
3719
|
+
if (trimmed === "") throw new UsageError(`the outcome is empty \u2014 say what the job should achieve${surface.outcomeHint}`);
|
|
3720
|
+
const length = graphemeLength(trimmed);
|
|
3721
|
+
if (length > OUTCOME_MAX_CHARACTERS) {
|
|
3722
|
+
throw new UsageError(
|
|
3723
|
+
`the outcome is ${length} characters once trimmed; a job takes at most ${OUTCOME_MAX_CHARACTERS}`
|
|
3724
|
+
);
|
|
3725
|
+
}
|
|
3726
|
+
const body = { outcome: trimmed };
|
|
3727
|
+
if (opts.budget !== void 0) {
|
|
3728
|
+
const budget = opts.budget.trim();
|
|
3729
|
+
const amount = BUDGET.test(budget) ? Number(budget) : NaN;
|
|
3730
|
+
if (!(amount > 0) || amount > BUDGET_MAX_DOLLARS) {
|
|
3731
|
+
throw new UsageError(
|
|
3732
|
+
`${surface.budget} takes a positive amount in US dollars with at most two decimals, up to ${BUDGET_MAX_DOLLARS}`
|
|
3733
|
+
);
|
|
3734
|
+
}
|
|
3735
|
+
body["budget"] = amount;
|
|
3736
|
+
}
|
|
3737
|
+
if (opts.skill !== void 0) {
|
|
3738
|
+
const skill = opts.skill.trim();
|
|
3739
|
+
if (skill.length > SKILL_MAX_CHARACTERS || !SKILL.test(skill)) {
|
|
3740
|
+
throw new UsageError(
|
|
3741
|
+
`${surface.skill} takes a skill's invocation name, like /summarize-lps (${surface.skills} lists them)`
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
body["skill"] = skill;
|
|
3745
|
+
}
|
|
3746
|
+
return { body };
|
|
3747
|
+
}
|
|
3748
|
+
var CLI_SURFACE = {
|
|
3749
|
+
budget: "--budget",
|
|
3750
|
+
skill: "--skill",
|
|
3751
|
+
outcomeHint: ': run "<outcome>"',
|
|
3752
|
+
skills: "`skills`",
|
|
3753
|
+
look: "`jobs --mine` for the new job",
|
|
3754
|
+
again: "run again"
|
|
3755
|
+
};
|
|
3756
|
+
var MCP_SURFACE = {
|
|
3757
|
+
budget: "budget",
|
|
3758
|
+
skill: "skill",
|
|
3759
|
+
outcomeHint: "",
|
|
3760
|
+
skills: "`skills_list`",
|
|
3761
|
+
look: "`jobs_list` with mine for the new job",
|
|
3762
|
+
again: "call again"
|
|
3763
|
+
};
|
|
3764
|
+
async function startRun(ctx, request, surface = CLI_SURFACE) {
|
|
3765
|
+
const write2 = { mayHave: "a job may have been started", check: surface.look };
|
|
3766
|
+
let data;
|
|
3767
|
+
try {
|
|
3768
|
+
data = (await sendOnce(ctx, "/api/v1/operator/jobs", request.body, write2)).data;
|
|
3769
|
+
} catch (error) {
|
|
3770
|
+
throw explainRefusal(error, "this Harness server has no Operator API for starting jobs yet", hints(error, surface));
|
|
3771
|
+
}
|
|
3772
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3773
|
+
throw outcomeUnknown("the answer named no job", write2);
|
|
3774
|
+
}
|
|
3775
|
+
const record = data;
|
|
3776
|
+
const jobId = record["job_id"];
|
|
3777
|
+
if (typeof jobId !== "string" || jobId === "") throw outcomeUnknown("the answer named no job", write2);
|
|
3778
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
3779
|
+
return {
|
|
3780
|
+
job_id: jobId,
|
|
3781
|
+
state: text(record["state"]),
|
|
3782
|
+
workbench_url: text(record["workbench_url"]),
|
|
3783
|
+
approval_url: text(record["approval_url"]) === "" ? null : text(record["approval_url"])
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
function hints(error, surface) {
|
|
3787
|
+
const retryAfter = error instanceof HttpError ? error.retryAfter : null;
|
|
3788
|
+
return {
|
|
3789
|
+
budget_not_permitted: `nothing was started: only the organization's admins, who manage its Operator budget, can pass ${surface.budget} \u2014 ${surface.again} without it`,
|
|
3790
|
+
budget_exceeded: `nothing was started: the organization's monthly Operator budget has less than ${surface.budget} left this month \u2014 pass a smaller budget, or ask an admin`,
|
|
3791
|
+
concurrent_change: `nothing was started: Operator or your access changed while the job started \u2014 check that you can still use Operator, then ${surface.again}`,
|
|
3792
|
+
invalid_outcome: `nothing was started: pass the outcome as text of 1 to ${OUTCOME_MAX_CHARACTERS} characters once trimmed, without control characters`,
|
|
3793
|
+
invalid_budget: `nothing was started: ${surface.budget} takes a positive amount in US dollars with at most two decimals, up to ${BUDGET_MAX_DOLLARS}`,
|
|
3794
|
+
unknown_skill: `nothing was started: no Operator skill of that name that you may run in the organization (${surface.skills} lists them)`,
|
|
3795
|
+
rate_limited: "nothing was started: you can start 30 jobs an hour this way" + (retryAfter === null ? "" : `; try again in ${clean(retryAfter)} s`),
|
|
3796
|
+
service_unavailable: "nothing was started; try again later"
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
function startedText(job) {
|
|
3800
|
+
return `Started Operator job ${clean(job.job_id)}.
|
|
3801
|
+
` + labelled([
|
|
3802
|
+
["State", clean(job.state) || null],
|
|
3803
|
+
["Workbench", clean(job.workbench_url) || null],
|
|
3804
|
+
["Approve at", job.approval_url === null ? null : clean(job.approval_url)]
|
|
3805
|
+
]) + (job.approval_url === null ? "" : "\nApproving happens in the app; the CLI cannot approve.\n");
|
|
3806
|
+
}
|
|
3807
|
+
async function run(ctx, request, opts = {}) {
|
|
3808
|
+
const job = await startRun(ctx, request);
|
|
3809
|
+
if (opts.watch !== true) return { json: job, human: startedText(job) };
|
|
3810
|
+
const json2 = opts.json === true;
|
|
3811
|
+
if (!json2) opts.onLine?.(startedText(job) + "\n");
|
|
3812
|
+
let watched;
|
|
3813
|
+
try {
|
|
3814
|
+
watched = await jobWatch(ctx, job.job_id, {
|
|
3815
|
+
throughWaits: true,
|
|
3816
|
+
collectEvents: json2,
|
|
3817
|
+
onLine: json2 ? void 0 : opts.onLine,
|
|
3818
|
+
signal: opts.signal,
|
|
3819
|
+
sleep: opts.sleep
|
|
3820
|
+
});
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
const follow = ` \u2014 the job was started: follow it with \`watch ${job.job_id}\``;
|
|
3823
|
+
if (error instanceof HttpError) {
|
|
3824
|
+
throw new HttpError(error.status, `${error.message}${follow}`, error.body, error.retryAfter);
|
|
3825
|
+
}
|
|
3826
|
+
if (error instanceof TransportError) throw new TransportError(`${error.message}${follow}`, error.code);
|
|
3827
|
+
if (error instanceof CommandError) throw new CommandError(`${error.message}${follow}`);
|
|
3828
|
+
throw error;
|
|
3829
|
+
}
|
|
3830
|
+
return { ...watched, json: { ...job, watch: watched.json } };
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3585
3833
|
// src/commands/operator-continue.ts
|
|
3586
3834
|
async function jobContinue(ctx, id) {
|
|
3587
3835
|
requireJobId(id);
|
|
@@ -3614,7 +3862,7 @@ function refusals(id) {
|
|
|
3614
3862
|
}
|
|
3615
3863
|
|
|
3616
3864
|
// src/core/textinput.ts
|
|
3617
|
-
import { readFileSync as
|
|
3865
|
+
import { readFileSync as readFileSync2, statSync as statSync3 } from "fs";
|
|
3618
3866
|
import { isatty } from "tty";
|
|
3619
3867
|
var STDIN_PATH = "-";
|
|
3620
3868
|
var MAX_TEXT_BYTES = 1e6;
|
|
@@ -3672,7 +3920,7 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
|
|
|
3672
3920
|
if (path2 === STDIN_PATH) {
|
|
3673
3921
|
let piped;
|
|
3674
3922
|
try {
|
|
3675
|
-
piped =
|
|
3923
|
+
piped = readFileSync2(0);
|
|
3676
3924
|
} catch (error) {
|
|
3677
3925
|
throw new UsageError(`${option} -: cannot read stdin (${describe(error)})`);
|
|
3678
3926
|
}
|
|
@@ -3684,7 +3932,7 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
|
|
|
3684
3932
|
const flag = `${option} ${path2}`;
|
|
3685
3933
|
let bytes;
|
|
3686
3934
|
try {
|
|
3687
|
-
bytes =
|
|
3935
|
+
bytes = readFileSync2(path2);
|
|
3688
3936
|
} catch (error) {
|
|
3689
3937
|
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
3690
3938
|
}
|
|
@@ -3882,14 +4130,14 @@ function requireProcedureName(name) {
|
|
|
3882
4130
|
}
|
|
3883
4131
|
async function procedureSave(ctx, jobId, name) {
|
|
3884
4132
|
requireJobId(jobId);
|
|
3885
|
-
const
|
|
4133
|
+
const named2 = requireProcedureName(name);
|
|
3886
4134
|
const write2 = {
|
|
3887
4135
|
mayHave: "a procedure may have been saved",
|
|
3888
4136
|
check: `the job's procedures with \`job ${jobId}\``
|
|
3889
4137
|
};
|
|
3890
4138
|
let data;
|
|
3891
4139
|
try {
|
|
3892
|
-
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${jobId}/procedures`, { name:
|
|
4140
|
+
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${jobId}/procedures`, { name: named2 }, write2)).data;
|
|
3893
4141
|
} catch (error) {
|
|
3894
4142
|
throw explainRefusal(error, missingJob(jobId), {
|
|
3895
4143
|
owner_required: "only the job's owner can save it as a procedure; ask them to save it",
|
|
@@ -4102,6 +4350,9 @@ import { parseDocument } from "yaml";
|
|
|
4102
4350
|
var PLAN_EXECUTORS = ["none", "operator"];
|
|
4103
4351
|
var CONDITION_KINDS = ["not_before", "gate", "event"];
|
|
4104
4352
|
var GATE_OWNER_ROLES = ["gp", "compliance_officer", "valuer", "accountant", "auditor", "counsel"];
|
|
4353
|
+
var PLAN_STATUSES = ["held", "released", "running", "completed", "needs_attention"];
|
|
4354
|
+
var PLAN_LIST_MAX_LIMIT = 100;
|
|
4355
|
+
var PLAN_LIST_DEFAULT_LIMIT = 50;
|
|
4105
4356
|
function parsePlanDocument(text, source) {
|
|
4106
4357
|
const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
|
|
4107
4358
|
if (!isRecord5(parsed)) {
|
|
@@ -4160,17 +4411,48 @@ async function planCreate(ctx, plan, opts) {
|
|
|
4160
4411
|
human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
|
|
4161
4412
|
|
|
4162
4413
|
` + planDetail(created) + `
|
|
4163
|
-
Nothing runs until it is released:
|
|
4414
|
+
Nothing runs until it is released: ${invokedName()} plan release ${clean(id)}
|
|
4164
4415
|
`
|
|
4165
4416
|
};
|
|
4166
4417
|
}
|
|
4418
|
+
function requirePlanListArgs(opts) {
|
|
4419
|
+
if (opts.status !== void 0 && !PLAN_STATUSES.includes(opts.status)) {
|
|
4420
|
+
throw new UsageError(`status must be one of: ${PLAN_STATUSES.join(", ")}`);
|
|
4421
|
+
}
|
|
4422
|
+
if (opts.limit !== void 0 && (!Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > PLAN_LIST_MAX_LIMIT)) {
|
|
4423
|
+
throw new UsageError(`--limit takes 1 to ${PLAN_LIST_MAX_LIMIT}: the server returns at most ${PLAN_LIST_MAX_LIMIT} plans`);
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
async function planList(ctx, opts) {
|
|
4427
|
+
requirePlanListArgs(opts);
|
|
4428
|
+
const team = requiredTeam(ctx, opts.team);
|
|
4429
|
+
let body;
|
|
4430
|
+
try {
|
|
4431
|
+
body = await ctx.client.get(`/api/v1/teams/${team}/plans`, { status: opts.status, limit: opts.limit });
|
|
4432
|
+
} catch (error) {
|
|
4433
|
+
throw explainMissing(error, `no team ${team} here, or this Harness server cannot list plans yet`);
|
|
4434
|
+
}
|
|
4435
|
+
if (!Array.isArray(body.data)) throw new CommandError("The plan list response carried no list.");
|
|
4436
|
+
const list3 = body.data.filter(isRecord5);
|
|
4437
|
+
const limit = opts.limit ?? PLAN_LIST_DEFAULT_LIMIT;
|
|
4438
|
+
let notice;
|
|
4439
|
+
if (list3.length >= limit) {
|
|
4440
|
+
notice = limit < PLAN_LIST_MAX_LIMIT ? `harness: showing the newest ${list3.length} plans \u2014 pass --limit (up to ${PLAN_LIST_MAX_LIMIT}) for older ones.
|
|
4441
|
+
` : `harness: showing the newest ${list3.length} plans, the most one list returns \u2014 narrow it with --status.
|
|
4442
|
+
`;
|
|
4443
|
+
}
|
|
4444
|
+
const what = opts.status === void 0 ? "plans" : `${opts.status} plans`;
|
|
4445
|
+
const human = list3.length === 0 ? `No ${what} in team ${team}.
|
|
4446
|
+
` : plansTable(list3);
|
|
4447
|
+
return notice === void 0 ? { json: body.data, human } : { json: body.data, human, notice };
|
|
4448
|
+
}
|
|
4167
4449
|
async function planShow(ctx, id) {
|
|
4168
|
-
|
|
4450
|
+
requirePlanRef(id);
|
|
4169
4451
|
const plan = await planRequest(id, () => ctx.client.get(`/api/v1/plans/${id}`));
|
|
4170
4452
|
return { json: plan, human: planDetail(plan) };
|
|
4171
4453
|
}
|
|
4172
4454
|
async function planEdit(ctx, id, edits) {
|
|
4173
|
-
|
|
4455
|
+
requirePlanRef(id);
|
|
4174
4456
|
requireDocument(edits, "a plan edit is one object: steps to change and budgets");
|
|
4175
4457
|
const plan = await planRequest(id, () => ctx.client.patch(`/api/v1/plans/${id}`, edits));
|
|
4176
4458
|
const held = Array.isArray(plan["held_steps"]) && plan["held_steps"].length > 0;
|
|
@@ -4179,12 +4461,12 @@ async function planEdit(ctx, id, edits) {
|
|
|
4179
4461
|
human: `Updated plan ${clean(id)}
|
|
4180
4462
|
|
|
4181
4463
|
` + planDetail(plan) + (held ? `
|
|
4182
|
-
|
|
4464
|
+
Only Operator steps are held until the next release; release the plan again to take in the changes: ${invokedName()} plan release ${clean(id)}
|
|
4183
4465
|
` : "")
|
|
4184
4466
|
};
|
|
4185
4467
|
}
|
|
4186
4468
|
async function planRelease(ctx, id, opts) {
|
|
4187
|
-
|
|
4469
|
+
requirePlanRef(id);
|
|
4188
4470
|
const body = {};
|
|
4189
4471
|
if (opts.collaborators !== void 0 && opts.collaborators.length > 0) body["collaborators"] = opts.collaborators;
|
|
4190
4472
|
if (opts.retryBudget !== void 0) body["retry_budget"] = opts.retryBudget;
|
|
@@ -4199,7 +4481,7 @@ async function planRelease(ctx, id, opts) {
|
|
|
4199
4481
|
};
|
|
4200
4482
|
}
|
|
4201
4483
|
async function planHold(ctx, id) {
|
|
4202
|
-
|
|
4484
|
+
requirePlanRef(id);
|
|
4203
4485
|
const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/hold`));
|
|
4204
4486
|
return {
|
|
4205
4487
|
json: plan,
|
|
@@ -4212,25 +4494,41 @@ async function gateSet(ctx, issue, gate, satisfied) {
|
|
|
4212
4494
|
const wanted = gate.trim();
|
|
4213
4495
|
if (wanted === "") throw new UsageError("name the gate by its key, its label or its condition id");
|
|
4214
4496
|
const expert = isExpertToken(ctx.config.token);
|
|
4215
|
-
|
|
4216
|
-
|
|
4497
|
+
let list3 = null;
|
|
4498
|
+
if (!isUuid(wanted)) {
|
|
4499
|
+
try {
|
|
4500
|
+
list3 = await readConditions(ctx, issue, expert);
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
if (error instanceof HttpError && error.status === 403) {
|
|
4503
|
+
throw new HttpError(
|
|
4504
|
+
error.status,
|
|
4505
|
+
`${error.message} \u2014 find the gate by its condition id; only Harness staff can list an issue's conditions`,
|
|
4506
|
+
error.body,
|
|
4507
|
+
error.retryAfter
|
|
4508
|
+
);
|
|
4509
|
+
}
|
|
4510
|
+
throw error;
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
const target = list3 === null ? { id: wanted.toLowerCase(), label: wanted.toLowerCase() } : findGate(list3, issue, wanted, expert);
|
|
4217
4514
|
const id = str7(target["id"]);
|
|
4218
4515
|
let body;
|
|
4219
4516
|
try {
|
|
4220
4517
|
body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
|
|
4221
4518
|
} catch (error) {
|
|
4222
|
-
throw await explainNotGateOwner(ctx, error, target, issue, satisfied);
|
|
4519
|
+
throw await explainNotGateOwner(ctx, error, target, issue, satisfied, list3 !== null);
|
|
4223
4520
|
}
|
|
4224
|
-
const others = list3
|
|
4521
|
+
const others = list3?.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length ?? 0;
|
|
4225
4522
|
let tail = "";
|
|
4226
|
-
if (satisfied && expert) {
|
|
4523
|
+
if (satisfied && list3 !== null && expert) {
|
|
4227
4524
|
tail = others > 0 ? ` \u2014 ${others} other gate${others === 1 ? "" : "s"} of yours on it ${others === 1 ? "is" : "are"} open` : "";
|
|
4228
|
-
} else if (satisfied) {
|
|
4525
|
+
} else if (satisfied && list3 !== null) {
|
|
4229
4526
|
tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
|
|
4230
4527
|
}
|
|
4528
|
+
const label = list3 === null && isRecord5(body.data) ? body.data["label"] ?? id : target["label"];
|
|
4231
4529
|
return {
|
|
4232
4530
|
json: body.data,
|
|
4233
|
-
human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(
|
|
4531
|
+
human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(label)}" on ${issue}${tail}
|
|
4234
4532
|
`
|
|
4235
4533
|
};
|
|
4236
4534
|
}
|
|
@@ -4264,8 +4562,10 @@ async function readConditions(ctx, issue, expert) {
|
|
|
4264
4562
|
if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
|
|
4265
4563
|
return conditions.filter(isRecord5);
|
|
4266
4564
|
}
|
|
4267
|
-
function
|
|
4268
|
-
if (!isUuid(id)
|
|
4565
|
+
function requirePlanRef(id) {
|
|
4566
|
+
if (!isUuid(id) && !isIdentifier(id)) {
|
|
4567
|
+
throw new UsageError(`not a plan: ${id} (expected the plan's id or its root issue's identifier, e.g. FR-1)`);
|
|
4568
|
+
}
|
|
4269
4569
|
}
|
|
4270
4570
|
function requireDocument(value, what) {
|
|
4271
4571
|
if (!isRecord5(value)) throw new UsageError(what);
|
|
@@ -4274,18 +4574,21 @@ async function planRequest(id, send) {
|
|
|
4274
4574
|
try {
|
|
4275
4575
|
return (await send()).data;
|
|
4276
4576
|
} catch (error) {
|
|
4277
|
-
throw explainMissing(
|
|
4577
|
+
throw explainMissing(
|
|
4578
|
+
error,
|
|
4579
|
+
isUuid(id) ? `no plan ${id} in this organization, or this Harness server has no plan API yet` : `${id} roots no plan in this organization (${invokedName()} show ${id} names the plan an issue belongs to, by its root), or this Harness server opens plans by their id only`
|
|
4580
|
+
);
|
|
4278
4581
|
}
|
|
4279
4582
|
}
|
|
4280
4583
|
function explainMissing(error, why) {
|
|
4281
4584
|
if (!(error instanceof HttpError) || error.status !== 404) return error;
|
|
4282
4585
|
return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
|
|
4283
4586
|
}
|
|
4284
|
-
async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
4587
|
+
async function explainNotGateOwner(ctx, error, gate, issue, satisfied, allowLookup) {
|
|
4285
4588
|
if (!(error instanceof HttpError) || error.status !== 403 || error.body["reason"] !== "not_gate_owner") return error;
|
|
4286
4589
|
let current;
|
|
4287
4590
|
let currentOwner = error.body["owner"];
|
|
4288
|
-
if (currentOwner !== null && !isRecord5(currentOwner)) {
|
|
4591
|
+
if (allowLookup && currentOwner !== null && !isRecord5(currentOwner)) {
|
|
4289
4592
|
try {
|
|
4290
4593
|
const fresh = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
|
|
4291
4594
|
if (Array.isArray(fresh)) current = fresh.filter(isRecord5).find((condition) => condition["id"] === gate["id"]);
|
|
@@ -4295,30 +4598,24 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
|
4295
4598
|
}
|
|
4296
4599
|
const owner = isRecord5(currentOwner) ? ownerText(currentOwner) : "";
|
|
4297
4600
|
const subject = `its owner${owner === "" ? "" : `, ${owner},`}`;
|
|
4298
|
-
const
|
|
4299
|
-
const message = satisfied ? `Cannot satisfy ${
|
|
4601
|
+
const named2 = `gate "${clean((current ?? gate)["label"])}" on ${issue}`;
|
|
4602
|
+
const message = satisfied ? `Cannot satisfy ${named2}: only ${subject} can tick it` : `Cannot un-satisfy ${named2}: only ${subject} or Harness staff can untick it`;
|
|
4300
4603
|
return new HttpError(error.status, message, error.body, error.retryAfter);
|
|
4301
4604
|
}
|
|
4302
4605
|
var gateLabel = (condition) => str7(condition["label"]).trim();
|
|
4303
4606
|
var gateKey = (condition) => str7(condition["key"]);
|
|
4304
4607
|
function exactMatches(conditions, wanted) {
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
if (condition["kind"] !== "gate") return false;
|
|
4309
|
-
const key = gateKey(condition);
|
|
4310
|
-
return key !== "" && key === (id ?? wanted) || gateLabel(condition) === wanted;
|
|
4311
|
-
});
|
|
4608
|
+
return conditions.filter(
|
|
4609
|
+
(condition) => condition["kind"] === "gate" && (gateKey(condition) !== "" && gateKey(condition) === wanted || gateLabel(condition) === wanted)
|
|
4610
|
+
);
|
|
4312
4611
|
}
|
|
4313
4612
|
function ambiguous(conditions, issue, wanted, matches) {
|
|
4314
4613
|
const listed = matches.map((condition) => {
|
|
4315
4614
|
const what = condition["kind"] === "gate" ? `gate ${gateName({ label: gateLabel(condition), key: gateKey(condition) })}` : `${clean(condition["kind"])} condition`;
|
|
4316
4615
|
return `${clean(condition["id"])} (${what})`;
|
|
4317
4616
|
}).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
4617
|
return new CommandError(
|
|
4321
|
-
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean
|
|
4618
|
+
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean`
|
|
4322
4619
|
);
|
|
4323
4620
|
}
|
|
4324
4621
|
function findGate(conditions, issue, wanted, expert = false) {
|
|
@@ -4326,11 +4623,6 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4326
4623
|
if (exact.length > 1) throw ambiguous(conditions, issue, wanted, exact);
|
|
4327
4624
|
if (exact.length === 1) {
|
|
4328
4625
|
const only = exact[0];
|
|
4329
|
-
if (only["kind"] !== "gate") {
|
|
4330
|
-
throw new CommandError(
|
|
4331
|
-
`condition ${wanted} on ${issue} is a ${clean(only["kind"])} condition \u2014 only a gate is satisfied by hand`
|
|
4332
|
-
);
|
|
4333
|
-
}
|
|
4334
4626
|
return only;
|
|
4335
4627
|
}
|
|
4336
4628
|
const gates = conditions.filter((condition) => condition["kind"] === "gate");
|
|
@@ -4340,7 +4632,6 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4340
4632
|
);
|
|
4341
4633
|
if (loose.length > 1) throw ambiguous(conditions, issue, wanted, loose);
|
|
4342
4634
|
if (loose.length === 1) return loose[0];
|
|
4343
|
-
if (isUuid(wanted)) throw new CommandError(`No condition ${wanted} on ${issue}${expert ? " that you own" : ""}.`);
|
|
4344
4635
|
const none = expert ? `No gate of yours on ${issue}` : `No gate on ${issue}`;
|
|
4345
4636
|
if (gates.length === 0) {
|
|
4346
4637
|
throw new CommandError(`${none} matches "${wanted}" \u2014 ${expert ? "you own no gate on it" : "it has no gates"}`);
|
|
@@ -4373,7 +4664,8 @@ function isRecord6(value) {
|
|
|
4373
4664
|
}
|
|
4374
4665
|
|
|
4375
4666
|
// src/core/credentials.ts
|
|
4376
|
-
import {
|
|
4667
|
+
import { createHash } from "crypto";
|
|
4668
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4377
4669
|
import { homedir } from "os";
|
|
4378
4670
|
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
4379
4671
|
function defaultCredentialsPath(env = process.env) {
|
|
@@ -4381,9 +4673,18 @@ function defaultCredentialsPath(env = process.env) {
|
|
|
4381
4673
|
const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
|
|
4382
4674
|
return join2(base, "harness", "credentials.json");
|
|
4383
4675
|
}
|
|
4676
|
+
function orgCredentialsPath(path2, baseUrl, org) {
|
|
4677
|
+
const url = new URL(baseUrl);
|
|
4678
|
+
const host = url.origin + url.pathname.replace(/\/+$/, "");
|
|
4679
|
+
const key = createHash("sha256").update(JSON.stringify([path2, host, org.toLowerCase()])).digest("hex");
|
|
4680
|
+
return join2(dirname2(path2), `credentials-org-${key}.json`);
|
|
4681
|
+
}
|
|
4682
|
+
function validOrgId(org) {
|
|
4683
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(org);
|
|
4684
|
+
}
|
|
4384
4685
|
function readCredentials(path2) {
|
|
4385
4686
|
try {
|
|
4386
|
-
const parsed = JSON.parse(
|
|
4687
|
+
const parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
4387
4688
|
if (typeof parsed === "object" && parsed !== null) {
|
|
4388
4689
|
const { url, token } = parsed;
|
|
4389
4690
|
if (typeof url === "string" && typeof token === "string") return { url, token };
|
|
@@ -4410,7 +4711,7 @@ function deleteCredentials(path2) {
|
|
|
4410
4711
|
}
|
|
4411
4712
|
|
|
4412
4713
|
// src/core/repo-config.ts
|
|
4413
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as
|
|
4714
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4414
4715
|
import { homedir as homedir2 } from "os";
|
|
4415
4716
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
|
|
4416
4717
|
var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
|
|
@@ -4429,7 +4730,7 @@ function findRepoConfig(startDir = process.cwd()) {
|
|
|
4429
4730
|
function readRepoConfig(path2) {
|
|
4430
4731
|
let parsed;
|
|
4431
4732
|
try {
|
|
4432
|
-
parsed = JSON.parse(
|
|
4733
|
+
parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
4433
4734
|
} catch (error) {
|
|
4434
4735
|
const detail = error instanceof Error ? error.message : String(error);
|
|
4435
4736
|
return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
|
|
@@ -4458,7 +4759,7 @@ function trustedHostsPath(env = process.env) {
|
|
|
4458
4759
|
}
|
|
4459
4760
|
function readTrustedHosts(env) {
|
|
4460
4761
|
try {
|
|
4461
|
-
const parsed = JSON.parse(
|
|
4762
|
+
const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
|
|
4462
4763
|
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
|
|
4463
4764
|
return parsed["hosts"].filter(
|
|
4464
4765
|
(host) => typeof host === "string"
|
|
@@ -4495,6 +4796,10 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4495
4796
|
const envUrl = blankToNull(env["HARNESS_API_URL"]);
|
|
4496
4797
|
const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
|
|
4497
4798
|
const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
|
|
4799
|
+
const org = envToken === null ? repo?.org?.toLowerCase() : void 0;
|
|
4800
|
+
if (org !== void 0 && !validOrgId(org)) {
|
|
4801
|
+
return { kind: "error", message: `repo org in ${repo?.path} must be an organization UUID`, warnings };
|
|
4802
|
+
}
|
|
4498
4803
|
let rawUrl;
|
|
4499
4804
|
let hostSource;
|
|
4500
4805
|
if (envUrl !== null) {
|
|
@@ -4513,6 +4818,9 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4513
4818
|
} else if (repoUrl !== null) {
|
|
4514
4819
|
rawUrl = repoUrl;
|
|
4515
4820
|
hostSource = "repo-config";
|
|
4821
|
+
} else if (org !== void 0) {
|
|
4822
|
+
rawUrl = DEFAULT_BASE_URL;
|
|
4823
|
+
hostSource = "default";
|
|
4516
4824
|
} else if (stored !== null) {
|
|
4517
4825
|
rawUrl = stored.url;
|
|
4518
4826
|
hostSource = "stored-login";
|
|
@@ -4523,13 +4831,16 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4523
4831
|
const validated = validateBaseUrl(rawUrl);
|
|
4524
4832
|
if (!validated.ok) return { kind: "error", message: validated.message, warnings };
|
|
4525
4833
|
const baseUrl = rawUrl.replace(/\/+$/, "");
|
|
4834
|
+
const selectedPath = org === void 0 ? path2 : orgCredentialsPath(path2, baseUrl, org);
|
|
4835
|
+
const selected = org === void 0 ? stored : readCredentials(selectedPath);
|
|
4526
4836
|
let token;
|
|
4527
4837
|
if (envToken !== null) {
|
|
4528
4838
|
token = envToken;
|
|
4529
|
-
} else if (
|
|
4530
|
-
token =
|
|
4839
|
+
} else if (selected !== null && normalize(selected.url) !== null && normalize(selected.url) === normalize(baseUrl)) {
|
|
4840
|
+
token = selected.token;
|
|
4531
4841
|
} else {
|
|
4532
|
-
const result = { kind: "needs-login", baseUrl, hostSource, warnings };
|
|
4842
|
+
const result = { kind: "needs-login", baseUrl, hostSource, credentialsPath: selectedPath, warnings };
|
|
4843
|
+
if (org !== void 0) result.org = org;
|
|
4533
4844
|
if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
|
|
4534
4845
|
return result;
|
|
4535
4846
|
}
|
|
@@ -4543,7 +4854,8 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4543
4854
|
// the --team flag at the command layer) stays authoritative.
|
|
4544
4855
|
team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
|
|
4545
4856
|
cacheDir: resolveCacheDir(env),
|
|
4546
|
-
hostSource
|
|
4857
|
+
hostSource,
|
|
4858
|
+
credentialsPath: selectedPath
|
|
4547
4859
|
},
|
|
4548
4860
|
warnings
|
|
4549
4861
|
};
|
|
@@ -4582,21 +4894,22 @@ function validateBaseUrl(baseUrl) {
|
|
|
4582
4894
|
}
|
|
4583
4895
|
if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
|
|
4584
4896
|
if (parsed.protocol === "http:") {
|
|
4585
|
-
|
|
4897
|
+
const host = parsed.hostname;
|
|
4898
|
+
if (["localhost", "127.0.0.1", "[::1]"].includes(host) || host.endsWith(".localhost")) return { ok: true };
|
|
4586
4899
|
return {
|
|
4587
4900
|
ok: false,
|
|
4588
|
-
message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost only.`
|
|
4901
|
+
message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost, *.localhost, 127.0.0.1 or [::1] only.`
|
|
4589
4902
|
};
|
|
4590
4903
|
}
|
|
4591
4904
|
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
4592
4905
|
}
|
|
4593
|
-
function loginHelp(baseUrl) {
|
|
4906
|
+
function loginHelp(baseUrl, name = invokedName()) {
|
|
4594
4907
|
return `Not signed in to ${baseUrl}.
|
|
4595
4908
|
|
|
4596
4909
|
Interactively, in a terminal (not from inside an agent \u2014 the browser
|
|
4597
4910
|
tab needs someone to see it):
|
|
4598
4911
|
|
|
4599
|
-
|
|
4912
|
+
${name} login
|
|
4600
4913
|
|
|
4601
4914
|
For agents and CI, mint an API token (admin \u2192 API tokens) with the
|
|
4602
4915
|
harness:read scope \u2014 plus harness:write only where the workflow
|
|
@@ -4605,7 +4918,7 @@ creates or closes \u2014 set an expiry, and export it:
|
|
|
4605
4918
|
export HARNESS_API_TOKEN=aone_...
|
|
4606
4919
|
|
|
4607
4920
|
An outside expert uses the pxc_ credential minted in their expert view:
|
|
4608
|
-
export it the same way, or paste it into
|
|
4921
|
+
export it the same way, or paste it into \`${name} login --no-browser\`.
|
|
4609
4922
|
`;
|
|
4610
4923
|
}
|
|
4611
4924
|
function blankToNull(value) {
|
|
@@ -4620,12 +4933,12 @@ export {
|
|
|
4620
4933
|
UsageError,
|
|
4621
4934
|
LocalError,
|
|
4622
4935
|
CommandError,
|
|
4623
|
-
CLI_VERSION,
|
|
4624
|
-
CLI_PACKAGE,
|
|
4625
4936
|
EXPERT_ASSIGNMENTS_PATH,
|
|
4626
4937
|
isExpertToken,
|
|
4627
4938
|
EXPERT_LAPSED,
|
|
4628
4939
|
explainExpertRefusal,
|
|
4940
|
+
SESSION_PATH,
|
|
4941
|
+
isLoginCredential,
|
|
4629
4942
|
Client,
|
|
4630
4943
|
clean,
|
|
4631
4944
|
json,
|
|
@@ -4719,6 +5032,12 @@ export {
|
|
|
4719
5032
|
jobShow,
|
|
4720
5033
|
jobWatch,
|
|
4721
5034
|
watchStopSignal,
|
|
5035
|
+
OUTCOME_MAX_CHARACTERS,
|
|
5036
|
+
BUDGET_MAX_DOLLARS,
|
|
5037
|
+
prepareRun,
|
|
5038
|
+
MCP_SURFACE,
|
|
5039
|
+
startRun,
|
|
5040
|
+
run,
|
|
4722
5041
|
jobContinue,
|
|
4723
5042
|
resolveTextSources,
|
|
4724
5043
|
STEER_MAX_CHARACTERS,
|
|
@@ -4736,17 +5055,23 @@ export {
|
|
|
4736
5055
|
PLAN_EXECUTORS,
|
|
4737
5056
|
CONDITION_KINDS,
|
|
4738
5057
|
GATE_OWNER_ROLES,
|
|
5058
|
+
PLAN_STATUSES,
|
|
5059
|
+
PLAN_LIST_MAX_LIMIT,
|
|
4739
5060
|
readPlanFile,
|
|
4740
5061
|
planCreate,
|
|
5062
|
+
requirePlanListArgs,
|
|
5063
|
+
planList,
|
|
4741
5064
|
planShow,
|
|
4742
5065
|
planEdit,
|
|
4743
5066
|
planRelease,
|
|
4744
5067
|
planHold,
|
|
4745
5068
|
gateSet,
|
|
4746
5069
|
conditionList,
|
|
4747
|
-
|
|
5070
|
+
requirePlanRef,
|
|
4748
5071
|
assignments,
|
|
4749
5072
|
defaultCredentialsPath,
|
|
5073
|
+
orgCredentialsPath,
|
|
5074
|
+
validOrgId,
|
|
4750
5075
|
readCredentials,
|
|
4751
5076
|
writeCredentials,
|
|
4752
5077
|
deleteCredentials,
|