@allocator-one/harness-cli 1.15.0 → 1.15.1
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 +53 -11
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-I2S2JVCQ.js → chunk-L2OWOZ4N.js} +240 -63
- package/dist/cli.js +185 -113
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-O7CX2ZCC.js → server-KVRQBVXI.js} +57 -12
- package/package.json +1 -1
- package/spec/openapi.json +1098 -293
|
@@ -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.
|
|
@@ -966,7 +986,7 @@ function path(record, ...keys) {
|
|
|
966
986
|
}
|
|
967
987
|
|
|
968
988
|
// src/commands/issues.ts
|
|
969
|
-
import { openAsBlob, openSync, closeSync, readFileSync
|
|
989
|
+
import { openAsBlob, openSync, closeSync, readFileSync, statSync as statSync2 } from "fs";
|
|
970
990
|
import { basename, extname } from "path";
|
|
971
991
|
|
|
972
992
|
// src/core/attachments.ts
|
|
@@ -1198,7 +1218,7 @@ function requireReadyClaimArgs(opts) {
|
|
|
1198
1218
|
async function claimNext(ctx, opts) {
|
|
1199
1219
|
requireReadyClaimArgs(opts);
|
|
1200
1220
|
const team = requiredTeam(ctx, opts.team);
|
|
1201
|
-
const check = `check
|
|
1221
|
+
const check = `check \`${invokedName()} list --team ${team} --assignee me --status in_progress\``;
|
|
1202
1222
|
const unreadable = (why) => new CommandError(`The claim-next response ${why}, so an issue may have been claimed \u2014 ${check}.`);
|
|
1203
1223
|
let body;
|
|
1204
1224
|
try {
|
|
@@ -1356,14 +1376,14 @@ async function prime(ctx) {
|
|
|
1356
1376
|
const statsBody = await ctx.client.get("/api/v1/stats");
|
|
1357
1377
|
const ready2 = markAgentSteps(await mostImportantReady(ctx));
|
|
1358
1378
|
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 }) + `
|
|
1379
|
+
const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + named(`
|
|
1360
1380
|
Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
|
|
1361
1381
|
harness close <id> --reason "Implemented in <commit-hash>" (response
|
|
1362
1382
|
lists newly unblocked issues \u2014 surface them).
|
|
1363
1383
|
Create tracked work BEFORE coding: harness create --title "\u2026" --type task.
|
|
1364
1384
|
Dependencies: harness dep add <issue> <blocker> (blocker blocks issue).
|
|
1365
1385
|
Before reporting done: close the issues you finished.
|
|
1366
|
-
|
|
1386
|
+
`);
|
|
1367
1387
|
return { json: json2, human };
|
|
1368
1388
|
}
|
|
1369
1389
|
async function mostImportantReady(ctx) {
|
|
@@ -1381,7 +1401,7 @@ async function version(config) {
|
|
|
1381
1401
|
} else {
|
|
1382
1402
|
server = await new Client(config).serverVersion().catch(() => "unreachable");
|
|
1383
1403
|
}
|
|
1384
|
-
return { json: { cli: CLI_VERSION, server }, human:
|
|
1404
|
+
return { json: { cli: CLI_VERSION, server }, human: `${invokedName()} ${CLI_VERSION} (server API ${server})
|
|
1385
1405
|
` };
|
|
1386
1406
|
}
|
|
1387
1407
|
async function create(ctx, opts) {
|
|
@@ -1496,8 +1516,8 @@ ${opts.appendNotes}`;
|
|
|
1496
1516
|
var CLAIM_REFUSAL_HINTS = {
|
|
1497
1517
|
// The edit holds a released step, so the plan needs releasing again before
|
|
1498
1518
|
// the step is claimable.
|
|
1499
|
-
agent_step: () =>
|
|
1500
|
-
waiting_on_condition: (identifier) =>
|
|
1519
|
+
agent_step: () => `Operator runs it for its plan; to do it yourself, set its executor to none (${invokedName()} plan edit <plan-id>) and release the plan again`,
|
|
1520
|
+
waiting_on_condition: (identifier) => `${invokedName()} show ${identifier} lists what it waits on; tick a gate with ${invokedName()} gate satisfy ${identifier} <gate>`
|
|
1501
1521
|
};
|
|
1502
1522
|
function explainClaimRefusal(error, identifier) {
|
|
1503
1523
|
if (!(error instanceof HttpError) || error.status !== 409) return error;
|
|
@@ -1604,9 +1624,9 @@ async function archive(ctx, identifiers, opts) {
|
|
|
1604
1624
|
}
|
|
1605
1625
|
async function comments(ctx, identifier) {
|
|
1606
1626
|
requireIdentifiers([identifier]);
|
|
1607
|
-
const
|
|
1608
|
-
if (
|
|
1609
|
-
const human =
|
|
1627
|
+
const records5 = await completeCommentThread(ctx, identifier);
|
|
1628
|
+
if (records5.length === 0) return { json: records5, human: "No comments.\n" };
|
|
1629
|
+
const human = records5.map((comment2) => {
|
|
1610
1630
|
const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
|
|
1611
1631
|
const id = clean(comment2["id"] ?? "unknown id");
|
|
1612
1632
|
const text = cleanMultiline(str2(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
|
|
@@ -1614,7 +1634,35 @@ async function comments(ctx, identifier) {
|
|
|
1614
1634
|
${text}
|
|
1615
1635
|
`;
|
|
1616
1636
|
}).join("\n");
|
|
1617
|
-
return { json:
|
|
1637
|
+
return { json: records5, human };
|
|
1638
|
+
}
|
|
1639
|
+
async function completeCommentThread(ctx, identifier) {
|
|
1640
|
+
const pages = [];
|
|
1641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1642
|
+
let before;
|
|
1643
|
+
for (; ; ) {
|
|
1644
|
+
const page = await ctx.client.get(`/api/v1/issues/${identifier}/comments`, {
|
|
1645
|
+
limit: 200,
|
|
1646
|
+
before
|
|
1647
|
+
});
|
|
1648
|
+
if (!Array.isArray(page.data) || typeof page.meta?.has_more !== "boolean") {
|
|
1649
|
+
throw new CommandError("Invalid comment pagination: expected a comment array and boolean meta.has_more; completeness unknown.");
|
|
1650
|
+
}
|
|
1651
|
+
const records5 = [];
|
|
1652
|
+
for (const record of page.data) {
|
|
1653
|
+
if (record === null || typeof record !== "object" || Array.isArray(record) || typeof record.id !== "string" || record.id.trim() === "" || seen.has(record.id)) {
|
|
1654
|
+
throw new CommandError("Invalid comment pagination: missing or repeated comment id; completeness unknown.");
|
|
1655
|
+
}
|
|
1656
|
+
seen.add(record.id);
|
|
1657
|
+
records5.push(record);
|
|
1658
|
+
}
|
|
1659
|
+
pages.push(records5);
|
|
1660
|
+
if (!page.meta.has_more) return pages.reverse().flat();
|
|
1661
|
+
if (records5.length === 0) {
|
|
1662
|
+
throw new CommandError("Invalid comment pagination: has_more on an empty page; completeness unknown.");
|
|
1663
|
+
}
|
|
1664
|
+
before = records5[0]["id"];
|
|
1665
|
+
}
|
|
1618
1666
|
}
|
|
1619
1667
|
async function dep(ctx, verb, issue, blocker) {
|
|
1620
1668
|
requireIdentifiers([issue, blocker]);
|
|
@@ -1764,7 +1812,7 @@ async function attachFile(ctx, path2) {
|
|
|
1764
1812
|
}
|
|
1765
1813
|
async function uploadAttachment(ctx, path2, maxBytes) {
|
|
1766
1814
|
attachableFile(path2, maxBytes);
|
|
1767
|
-
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([
|
|
1815
|
+
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([readFileSync(path2)], basename(path2));
|
|
1768
1816
|
requireAttachmentSize(path2, file.size, maxBytes);
|
|
1769
1817
|
const form = new FormData();
|
|
1770
1818
|
form.set("file", file);
|
|
@@ -1871,13 +1919,13 @@ async function prepareApplicableLabelIds(ctx, teamId, names) {
|
|
|
1871
1919
|
}
|
|
1872
1920
|
function applicableLabel(labels, input, teamId) {
|
|
1873
1921
|
const normalized = normalizeLabelName(input);
|
|
1874
|
-
const
|
|
1875
|
-
const teamMatches =
|
|
1922
|
+
const named2 = labels.filter((label) => normalizeLabelName(label["name"]) === normalized);
|
|
1923
|
+
const teamMatches = named2.filter((label) => label["team_id"] === teamId);
|
|
1876
1924
|
if (teamMatches.length > 1) {
|
|
1877
1925
|
throw new CommandError(`more than one team label matches "${input}"; use a label id`);
|
|
1878
1926
|
}
|
|
1879
1927
|
if (teamMatches.length === 1) return teamMatches[0];
|
|
1880
|
-
const workspaceMatches =
|
|
1928
|
+
const workspaceMatches = named2.filter(
|
|
1881
1929
|
(label) => label["team_id"] === null || label["team_id"] === void 0
|
|
1882
1930
|
);
|
|
1883
1931
|
if (workspaceMatches.length > 1) {
|
|
@@ -2003,8 +2051,8 @@ function listParams(ctx, opts) {
|
|
|
2003
2051
|
}
|
|
2004
2052
|
function optionalPriority(value) {
|
|
2005
2053
|
if (value === void 0) return null;
|
|
2006
|
-
const
|
|
2007
|
-
if (
|
|
2054
|
+
const named2 = Object.hasOwn(PRIORITY_NAMES, value) ? PRIORITY_NAMES[value] : void 0;
|
|
2055
|
+
if (named2 !== void 0) return named2;
|
|
2008
2056
|
if (/^\d+$/.test(value)) {
|
|
2009
2057
|
throw new UsageError(
|
|
2010
2058
|
`Bare numeric priorities are refused: bd and harness disagree on what
|
|
@@ -2518,7 +2566,7 @@ var APPROVAL_NOTE = "Write and send capabilities, and any proposal, need a perso
|
|
|
2518
2566
|
var REFUSALS = {
|
|
2519
2567
|
operator_disabled: "nothing here works until it is switched back on",
|
|
2520
2568
|
operator_not_permitted: "this account has no admin or member role in the organization the credential is bound to",
|
|
2521
|
-
actor_required:
|
|
2569
|
+
actor_required: `this credential acts as no person: sign in with \`${invokedName()} login\`, or use an API token minted for a user`,
|
|
2522
2570
|
run_bound_credential: "a run-bound credential can't read Operator's catalog",
|
|
2523
2571
|
forbidden: "Operator needs the operator:read scope (aone_ API tokens must be minted with it) and an admin or member role"
|
|
2524
2572
|
};
|
|
@@ -2638,7 +2686,7 @@ function requireCapabilityId(id) {
|
|
|
2638
2686
|
throw new UsageError(`not a capability id: ${clean(id)} (e.g. crm.party.search)`);
|
|
2639
2687
|
}
|
|
2640
2688
|
if (id === "domains") {
|
|
2641
|
-
throw new UsageError(
|
|
2689
|
+
throw new UsageError(`domains is not a capability id \u2014 \`${invokedName()} tools domains\` lists the domains`);
|
|
2642
2690
|
}
|
|
2643
2691
|
}
|
|
2644
2692
|
function requireSearchQuery(query) {
|
|
@@ -2995,9 +3043,9 @@ function jobPeople(job) {
|
|
|
2995
3043
|
const participants = records3(field2(job, "people", "participants"));
|
|
2996
3044
|
const owner = isRecord2(job["owner"]) ? job["owner"] : null;
|
|
2997
3045
|
const names = /* @__PURE__ */ new Map();
|
|
2998
|
-
for (const
|
|
2999
|
-
if (typeof
|
|
3000
|
-
if (!names.has(
|
|
3046
|
+
for (const named2 of owner === null ? participants : [...participants, owner]) {
|
|
3047
|
+
if (typeof named2["user_id"] === "string" && typeof named2["name"] === "string" && named2["name"] !== "") {
|
|
3048
|
+
if (!names.has(named2["user_id"])) names.set(named2["user_id"], named2["name"]);
|
|
3001
3049
|
}
|
|
3002
3050
|
}
|
|
3003
3051
|
const person = (userId) => typeof userId === "string" && userId !== "" ? { user_id: userId, name: names.get(userId) ?? null } : null;
|
|
@@ -3090,7 +3138,8 @@ async function jobWatch(ctx, id, opts) {
|
|
|
3090
3138
|
}
|
|
3091
3139
|
}
|
|
3092
3140
|
function resumeLine(id, after) {
|
|
3093
|
-
|
|
3141
|
+
const name = invokedName();
|
|
3142
|
+
return `${name}: stopped watching job ${id}; resume with: ${name} watch ${id} --after ${after}
|
|
3094
3143
|
`;
|
|
3095
3144
|
}
|
|
3096
3145
|
function watchStopSignal(stdout = process.stdout, signals = process) {
|
|
@@ -3179,9 +3228,9 @@ async function eventsPage(ctx, id, after) {
|
|
|
3179
3228
|
var REFUSAL_HINTS = {
|
|
3180
3229
|
operator_disabled: "nothing can be read until Operator is switched back on for this organization",
|
|
3181
3230
|
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:
|
|
3231
|
+
actor_required: `the credential acts as no person: sign in with \`${invokedName()} login\`, or use an API token a person created`,
|
|
3232
|
+
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`,
|
|
3233
|
+
run_bound_credential: `a run-bound credential cannot read Operator jobs; sign in with \`${invokedName()} login\``
|
|
3185
3234
|
};
|
|
3186
3235
|
function missingJob(id) {
|
|
3187
3236
|
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 +3616,10 @@ async function sendOnce(ctx, path2, body, write2) {
|
|
|
3567
3616
|
throw error;
|
|
3568
3617
|
}
|
|
3569
3618
|
}
|
|
3570
|
-
function explainRefusal(error, notFound2,
|
|
3619
|
+
function explainRefusal(error, notFound2, hints2) {
|
|
3571
3620
|
if (!(error instanceof HttpError) || error instanceof UpgradeRequiredError) return error;
|
|
3572
3621
|
const code = typeof error.body["error"] === "string" ? error.body["error"] : "";
|
|
3573
|
-
const own = Object.hasOwn(
|
|
3622
|
+
const own = Object.hasOwn(hints2, code) ? hints2[code] : void 0;
|
|
3574
3623
|
let hint;
|
|
3575
3624
|
if (error.status >= 500) hint = own;
|
|
3576
3625
|
else if (error.status === 404) hint = notFound2;
|
|
@@ -3578,10 +3627,132 @@ function explainRefusal(error, notFound2, hints) {
|
|
|
3578
3627
|
else if (error.status === 403 && Object.hasOwn(ACCESS_HINTS, code)) hint = ACCESS_HINTS[code];
|
|
3579
3628
|
else if (error.status === 400 && code === "bad_request") hint = BAD_REQUEST_HINT;
|
|
3580
3629
|
if (hint === void 0) return error;
|
|
3581
|
-
const message = error.status
|
|
3630
|
+
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
3631
|
return new HttpError(error.status, `${message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
3583
3632
|
}
|
|
3584
3633
|
|
|
3634
|
+
// src/commands/operator-run.ts
|
|
3635
|
+
var OUTCOME_MAX_CHARACTERS = 1e4;
|
|
3636
|
+
var BUDGET_MAX_DOLLARS = 1e6;
|
|
3637
|
+
var SKILL = /^\/?[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3638
|
+
var SKILL_MAX_CHARACTERS = 65;
|
|
3639
|
+
var BUDGET = /^\d+(\.\d{1,2})?$/;
|
|
3640
|
+
function prepareRun(outcome, opts = {}, surface = CLI_SURFACE) {
|
|
3641
|
+
const trimmed = outcome.trim();
|
|
3642
|
+
if (trimmed === "") throw new UsageError(`the outcome is empty \u2014 say what the job should achieve${surface.outcomeHint}`);
|
|
3643
|
+
const length = graphemeLength(trimmed);
|
|
3644
|
+
if (length > OUTCOME_MAX_CHARACTERS) {
|
|
3645
|
+
throw new UsageError(
|
|
3646
|
+
`the outcome is ${length} characters once trimmed; a job takes at most ${OUTCOME_MAX_CHARACTERS}`
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
const body = { outcome: trimmed };
|
|
3650
|
+
if (opts.budget !== void 0) {
|
|
3651
|
+
const budget = opts.budget.trim();
|
|
3652
|
+
const amount = BUDGET.test(budget) ? Number(budget) : NaN;
|
|
3653
|
+
if (!(amount > 0) || amount > BUDGET_MAX_DOLLARS) {
|
|
3654
|
+
throw new UsageError(
|
|
3655
|
+
`${surface.budget} takes a positive amount in US dollars with at most two decimals, up to ${BUDGET_MAX_DOLLARS}`
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
body["budget"] = amount;
|
|
3659
|
+
}
|
|
3660
|
+
if (opts.skill !== void 0) {
|
|
3661
|
+
const skill = opts.skill.trim();
|
|
3662
|
+
if (skill.length > SKILL_MAX_CHARACTERS || !SKILL.test(skill)) {
|
|
3663
|
+
throw new UsageError(
|
|
3664
|
+
`${surface.skill} takes a skill's invocation name, like /summarize-lps (${surface.skills} lists them)`
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
body["skill"] = skill;
|
|
3668
|
+
}
|
|
3669
|
+
return { body };
|
|
3670
|
+
}
|
|
3671
|
+
var CLI_SURFACE = {
|
|
3672
|
+
budget: "--budget",
|
|
3673
|
+
skill: "--skill",
|
|
3674
|
+
outcomeHint: ': run "<outcome>"',
|
|
3675
|
+
skills: "`skills`",
|
|
3676
|
+
look: "`jobs --mine` for the new job",
|
|
3677
|
+
again: "run again"
|
|
3678
|
+
};
|
|
3679
|
+
var MCP_SURFACE = {
|
|
3680
|
+
budget: "budget",
|
|
3681
|
+
skill: "skill",
|
|
3682
|
+
outcomeHint: "",
|
|
3683
|
+
skills: "`skills_list`",
|
|
3684
|
+
look: "`jobs_list` with mine for the new job",
|
|
3685
|
+
again: "call again"
|
|
3686
|
+
};
|
|
3687
|
+
async function startRun(ctx, request, surface = CLI_SURFACE) {
|
|
3688
|
+
const write2 = { mayHave: "a job may have been started", check: surface.look };
|
|
3689
|
+
let data;
|
|
3690
|
+
try {
|
|
3691
|
+
data = (await sendOnce(ctx, "/api/v1/operator/jobs", request.body, write2)).data;
|
|
3692
|
+
} catch (error) {
|
|
3693
|
+
throw explainRefusal(error, "this Harness server has no Operator API for starting jobs yet", hints(error, surface));
|
|
3694
|
+
}
|
|
3695
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3696
|
+
throw outcomeUnknown("the answer named no job", write2);
|
|
3697
|
+
}
|
|
3698
|
+
const record = data;
|
|
3699
|
+
const jobId = record["job_id"];
|
|
3700
|
+
if (typeof jobId !== "string" || jobId === "") throw outcomeUnknown("the answer named no job", write2);
|
|
3701
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
3702
|
+
return {
|
|
3703
|
+
job_id: jobId,
|
|
3704
|
+
state: text(record["state"]),
|
|
3705
|
+
workbench_url: text(record["workbench_url"]),
|
|
3706
|
+
approval_url: text(record["approval_url"]) === "" ? null : text(record["approval_url"])
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
function hints(error, surface) {
|
|
3710
|
+
const retryAfter = error instanceof HttpError ? error.retryAfter : null;
|
|
3711
|
+
return {
|
|
3712
|
+
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`,
|
|
3713
|
+
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`,
|
|
3714
|
+
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}`,
|
|
3715
|
+
invalid_outcome: `nothing was started: pass the outcome as text of 1 to ${OUTCOME_MAX_CHARACTERS} characters once trimmed, without control characters`,
|
|
3716
|
+
invalid_budget: `nothing was started: ${surface.budget} takes a positive amount in US dollars with at most two decimals, up to ${BUDGET_MAX_DOLLARS}`,
|
|
3717
|
+
unknown_skill: `nothing was started: no Operator skill of that name that you may run in the organization (${surface.skills} lists them)`,
|
|
3718
|
+
rate_limited: "nothing was started: you can start 30 jobs an hour this way" + (retryAfter === null ? "" : `; try again in ${clean(retryAfter)} s`),
|
|
3719
|
+
service_unavailable: "nothing was started; try again later"
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
function startedText(job) {
|
|
3723
|
+
return `Started Operator job ${clean(job.job_id)}.
|
|
3724
|
+
` + labelled([
|
|
3725
|
+
["State", clean(job.state) || null],
|
|
3726
|
+
["Workbench", clean(job.workbench_url) || null],
|
|
3727
|
+
["Approve at", job.approval_url === null ? null : clean(job.approval_url)]
|
|
3728
|
+
]) + (job.approval_url === null ? "" : "\nApproving happens in the app; the CLI cannot approve.\n");
|
|
3729
|
+
}
|
|
3730
|
+
async function run(ctx, request, opts = {}) {
|
|
3731
|
+
const job = await startRun(ctx, request);
|
|
3732
|
+
if (opts.watch !== true) return { json: job, human: startedText(job) };
|
|
3733
|
+
const json2 = opts.json === true;
|
|
3734
|
+
if (!json2) opts.onLine?.(startedText(job) + "\n");
|
|
3735
|
+
let watched;
|
|
3736
|
+
try {
|
|
3737
|
+
watched = await jobWatch(ctx, job.job_id, {
|
|
3738
|
+
throughWaits: true,
|
|
3739
|
+
collectEvents: json2,
|
|
3740
|
+
onLine: json2 ? void 0 : opts.onLine,
|
|
3741
|
+
signal: opts.signal,
|
|
3742
|
+
sleep: opts.sleep
|
|
3743
|
+
});
|
|
3744
|
+
} catch (error) {
|
|
3745
|
+
const follow = ` \u2014 the job was started: follow it with \`watch ${job.job_id}\``;
|
|
3746
|
+
if (error instanceof HttpError) {
|
|
3747
|
+
throw new HttpError(error.status, `${error.message}${follow}`, error.body, error.retryAfter);
|
|
3748
|
+
}
|
|
3749
|
+
if (error instanceof TransportError) throw new TransportError(`${error.message}${follow}`, error.code);
|
|
3750
|
+
if (error instanceof CommandError) throw new CommandError(`${error.message}${follow}`);
|
|
3751
|
+
throw error;
|
|
3752
|
+
}
|
|
3753
|
+
return { ...watched, json: { ...job, watch: watched.json } };
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3585
3756
|
// src/commands/operator-continue.ts
|
|
3586
3757
|
async function jobContinue(ctx, id) {
|
|
3587
3758
|
requireJobId(id);
|
|
@@ -3614,7 +3785,7 @@ function refusals(id) {
|
|
|
3614
3785
|
}
|
|
3615
3786
|
|
|
3616
3787
|
// src/core/textinput.ts
|
|
3617
|
-
import { readFileSync as
|
|
3788
|
+
import { readFileSync as readFileSync2, statSync as statSync3 } from "fs";
|
|
3618
3789
|
import { isatty } from "tty";
|
|
3619
3790
|
var STDIN_PATH = "-";
|
|
3620
3791
|
var MAX_TEXT_BYTES = 1e6;
|
|
@@ -3672,7 +3843,7 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
|
|
|
3672
3843
|
if (path2 === STDIN_PATH) {
|
|
3673
3844
|
let piped;
|
|
3674
3845
|
try {
|
|
3675
|
-
piped =
|
|
3846
|
+
piped = readFileSync2(0);
|
|
3676
3847
|
} catch (error) {
|
|
3677
3848
|
throw new UsageError(`${option} -: cannot read stdin (${describe(error)})`);
|
|
3678
3849
|
}
|
|
@@ -3684,7 +3855,7 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
|
|
|
3684
3855
|
const flag = `${option} ${path2}`;
|
|
3685
3856
|
let bytes;
|
|
3686
3857
|
try {
|
|
3687
|
-
bytes =
|
|
3858
|
+
bytes = readFileSync2(path2);
|
|
3688
3859
|
} catch (error) {
|
|
3689
3860
|
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
3690
3861
|
}
|
|
@@ -3882,14 +4053,14 @@ function requireProcedureName(name) {
|
|
|
3882
4053
|
}
|
|
3883
4054
|
async function procedureSave(ctx, jobId, name) {
|
|
3884
4055
|
requireJobId(jobId);
|
|
3885
|
-
const
|
|
4056
|
+
const named2 = requireProcedureName(name);
|
|
3886
4057
|
const write2 = {
|
|
3887
4058
|
mayHave: "a procedure may have been saved",
|
|
3888
4059
|
check: `the job's procedures with \`job ${jobId}\``
|
|
3889
4060
|
};
|
|
3890
4061
|
let data;
|
|
3891
4062
|
try {
|
|
3892
|
-
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${jobId}/procedures`, { name:
|
|
4063
|
+
data = (await sendOnce(ctx, `/api/v1/operator/jobs/${jobId}/procedures`, { name: named2 }, write2)).data;
|
|
3893
4064
|
} catch (error) {
|
|
3894
4065
|
throw explainRefusal(error, missingJob(jobId), {
|
|
3895
4066
|
owner_required: "only the job's owner can save it as a procedure; ask them to save it",
|
|
@@ -4160,7 +4331,7 @@ async function planCreate(ctx, plan, opts) {
|
|
|
4160
4331
|
human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
|
|
4161
4332
|
|
|
4162
4333
|
` + planDetail(created) + `
|
|
4163
|
-
Nothing runs until it is released:
|
|
4334
|
+
Nothing runs until it is released: ${invokedName()} plan release ${clean(id)}
|
|
4164
4335
|
`
|
|
4165
4336
|
};
|
|
4166
4337
|
}
|
|
@@ -4179,7 +4350,7 @@ async function planEdit(ctx, id, edits) {
|
|
|
4179
4350
|
human: `Updated plan ${clean(id)}
|
|
4180
4351
|
|
|
4181
4352
|
` + planDetail(plan) + (held ? `
|
|
4182
|
-
Release it again to run the held steps:
|
|
4353
|
+
Release it again to run the held steps: ${invokedName()} plan release ${clean(id)}
|
|
4183
4354
|
` : "")
|
|
4184
4355
|
};
|
|
4185
4356
|
}
|
|
@@ -4295,8 +4466,8 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
|
4295
4466
|
}
|
|
4296
4467
|
const owner = isRecord5(currentOwner) ? ownerText(currentOwner) : "";
|
|
4297
4468
|
const subject = `its owner${owner === "" ? "" : `, ${owner},`}`;
|
|
4298
|
-
const
|
|
4299
|
-
const message = satisfied ? `Cannot satisfy ${
|
|
4469
|
+
const named2 = `gate "${clean((current ?? gate)["label"])}" on ${issue}`;
|
|
4470
|
+
const message = satisfied ? `Cannot satisfy ${named2}: only ${subject} can tick it` : `Cannot un-satisfy ${named2}: only ${subject} or Harness staff can untick it`;
|
|
4300
4471
|
return new HttpError(error.status, message, error.body, error.retryAfter);
|
|
4301
4472
|
}
|
|
4302
4473
|
var gateLabel = (condition) => str7(condition["label"]).trim();
|
|
@@ -4373,7 +4544,7 @@ function isRecord6(value) {
|
|
|
4373
4544
|
}
|
|
4374
4545
|
|
|
4375
4546
|
// src/core/credentials.ts
|
|
4376
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as
|
|
4547
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4377
4548
|
import { homedir } from "os";
|
|
4378
4549
|
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
4379
4550
|
function defaultCredentialsPath(env = process.env) {
|
|
@@ -4383,7 +4554,7 @@ function defaultCredentialsPath(env = process.env) {
|
|
|
4383
4554
|
}
|
|
4384
4555
|
function readCredentials(path2) {
|
|
4385
4556
|
try {
|
|
4386
|
-
const parsed = JSON.parse(
|
|
4557
|
+
const parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
4387
4558
|
if (typeof parsed === "object" && parsed !== null) {
|
|
4388
4559
|
const { url, token } = parsed;
|
|
4389
4560
|
if (typeof url === "string" && typeof token === "string") return { url, token };
|
|
@@ -4410,7 +4581,7 @@ function deleteCredentials(path2) {
|
|
|
4410
4581
|
}
|
|
4411
4582
|
|
|
4412
4583
|
// src/core/repo-config.ts
|
|
4413
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as
|
|
4584
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4414
4585
|
import { homedir as homedir2 } from "os";
|
|
4415
4586
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
|
|
4416
4587
|
var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
|
|
@@ -4429,7 +4600,7 @@ function findRepoConfig(startDir = process.cwd()) {
|
|
|
4429
4600
|
function readRepoConfig(path2) {
|
|
4430
4601
|
let parsed;
|
|
4431
4602
|
try {
|
|
4432
|
-
parsed = JSON.parse(
|
|
4603
|
+
parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
4433
4604
|
} catch (error) {
|
|
4434
4605
|
const detail = error instanceof Error ? error.message : String(error);
|
|
4435
4606
|
return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
|
|
@@ -4458,7 +4629,7 @@ function trustedHostsPath(env = process.env) {
|
|
|
4458
4629
|
}
|
|
4459
4630
|
function readTrustedHosts(env) {
|
|
4460
4631
|
try {
|
|
4461
|
-
const parsed = JSON.parse(
|
|
4632
|
+
const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
|
|
4462
4633
|
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
|
|
4463
4634
|
return parsed["hosts"].filter(
|
|
4464
4635
|
(host) => typeof host === "string"
|
|
@@ -4590,13 +4761,13 @@ function validateBaseUrl(baseUrl) {
|
|
|
4590
4761
|
}
|
|
4591
4762
|
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
4592
4763
|
}
|
|
4593
|
-
function loginHelp(baseUrl) {
|
|
4764
|
+
function loginHelp(baseUrl, name = invokedName()) {
|
|
4594
4765
|
return `Not signed in to ${baseUrl}.
|
|
4595
4766
|
|
|
4596
4767
|
Interactively, in a terminal (not from inside an agent \u2014 the browser
|
|
4597
4768
|
tab needs someone to see it):
|
|
4598
4769
|
|
|
4599
|
-
|
|
4770
|
+
${name} login
|
|
4600
4771
|
|
|
4601
4772
|
For agents and CI, mint an API token (admin \u2192 API tokens) with the
|
|
4602
4773
|
harness:read scope \u2014 plus harness:write only where the workflow
|
|
@@ -4605,7 +4776,7 @@ creates or closes \u2014 set an expiry, and export it:
|
|
|
4605
4776
|
export HARNESS_API_TOKEN=aone_...
|
|
4606
4777
|
|
|
4607
4778
|
An outside expert uses the pxc_ credential minted in their expert view:
|
|
4608
|
-
export it the same way, or paste it into
|
|
4779
|
+
export it the same way, or paste it into \`${name} login --no-browser\`.
|
|
4609
4780
|
`;
|
|
4610
4781
|
}
|
|
4611
4782
|
function blankToNull(value) {
|
|
@@ -4620,12 +4791,12 @@ export {
|
|
|
4620
4791
|
UsageError,
|
|
4621
4792
|
LocalError,
|
|
4622
4793
|
CommandError,
|
|
4623
|
-
CLI_VERSION,
|
|
4624
|
-
CLI_PACKAGE,
|
|
4625
4794
|
EXPERT_ASSIGNMENTS_PATH,
|
|
4626
4795
|
isExpertToken,
|
|
4627
4796
|
EXPERT_LAPSED,
|
|
4628
4797
|
explainExpertRefusal,
|
|
4798
|
+
SESSION_PATH,
|
|
4799
|
+
isLoginCredential,
|
|
4629
4800
|
Client,
|
|
4630
4801
|
clean,
|
|
4631
4802
|
json,
|
|
@@ -4719,6 +4890,12 @@ export {
|
|
|
4719
4890
|
jobShow,
|
|
4720
4891
|
jobWatch,
|
|
4721
4892
|
watchStopSignal,
|
|
4893
|
+
OUTCOME_MAX_CHARACTERS,
|
|
4894
|
+
BUDGET_MAX_DOLLARS,
|
|
4895
|
+
prepareRun,
|
|
4896
|
+
MCP_SURFACE,
|
|
4897
|
+
startRun,
|
|
4898
|
+
run,
|
|
4722
4899
|
jobContinue,
|
|
4723
4900
|
resolveTextSources,
|
|
4724
4901
|
STEER_MAX_CHARACTERS,
|