@allocator-one/harness-cli 1.16.0 → 1.17.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 +115 -0
- package/dist/{chunk-FQWV5MIC.js → chunk-5THYIKLW.js} +26 -4
- package/dist/chunk-A4WZAC7L.js +563 -0
- package/dist/chunk-BHBEIT5I.js +1348 -0
- package/dist/{chunk-GVTZKAXC.js → chunk-CEEQW6ZV.js} +299 -1592
- package/dist/chunk-W7BVOQQG.js +1448 -0
- package/dist/chunk-WSDFLMO6.js +717 -0
- package/dist/cli.js +1652 -1146
- package/dist/infraone-A3KDADRM.js +249 -0
- package/dist/infraone-V3IKKVBG.js +593 -0
- package/dist/infraone.js +372 -0
- package/dist/{login-page-KGEJFFXR.js → login-page-MFHNVTUV.js} +1 -1
- package/dist/{server-F6YRKNC2.js → server-TRXBTJSA.js} +11 -9
- package/package.json +4 -3
- package/skills/harness-code/SKILL.md +37 -0
- package/spec/openapi.json +3411 -0
|
@@ -1,1039 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
Client,
|
|
4
|
+
CommandError,
|
|
5
|
+
EXPERT_ASSIGNMENTS_PATH,
|
|
6
|
+
HttpError,
|
|
7
|
+
LocalError,
|
|
8
|
+
MalformedResponseError,
|
|
9
|
+
TransportError,
|
|
10
|
+
UpgradeRequiredError,
|
|
11
|
+
UsageError,
|
|
12
|
+
activityEntries,
|
|
13
|
+
clean,
|
|
14
|
+
cleanMultiline,
|
|
15
|
+
conditionLine,
|
|
16
|
+
errorDetails,
|
|
17
|
+
expertAssignments,
|
|
18
|
+
expertSummary,
|
|
19
|
+
explainExpertRefusal,
|
|
20
|
+
gateName,
|
|
21
|
+
graphemeLength,
|
|
22
|
+
isAgentStep,
|
|
23
|
+
isExpertToken,
|
|
24
|
+
issueDetail,
|
|
25
|
+
issuesTable,
|
|
26
|
+
ownerText,
|
|
27
|
+
planDetail,
|
|
28
|
+
plansTable,
|
|
29
|
+
stats,
|
|
30
|
+
table,
|
|
31
|
+
truncate
|
|
32
|
+
} from "./chunk-BHBEIT5I.js";
|
|
33
|
+
import {
|
|
4
34
|
CLI_VERSION,
|
|
5
35
|
invokedName,
|
|
6
36
|
named
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
|
|
9
|
-
// src/core/errors.ts
|
|
10
|
-
var HttpError = class extends Error {
|
|
11
|
-
constructor(status, message, body = {}, retryAfter = null) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.status = status;
|
|
14
|
-
this.body = body;
|
|
15
|
-
this.retryAfter = retryAfter;
|
|
16
|
-
this.name = "HttpError";
|
|
17
|
-
}
|
|
18
|
-
status;
|
|
19
|
-
body;
|
|
20
|
-
retryAfter;
|
|
21
|
-
};
|
|
22
|
-
var UpgradeRequiredError = class extends HttpError {
|
|
23
|
-
constructor(message, body = {}) {
|
|
24
|
-
super(426, message, body);
|
|
25
|
-
this.name = "UpgradeRequiredError";
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
var TransportError = class extends Error {
|
|
29
|
-
constructor(message, code = null) {
|
|
30
|
-
super(message);
|
|
31
|
-
this.code = code;
|
|
32
|
-
this.name = "TransportError";
|
|
33
|
-
}
|
|
34
|
-
code;
|
|
35
|
-
};
|
|
36
|
-
var MalformedResponseError = class extends TransportError {
|
|
37
|
-
constructor(message) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.name = "MalformedResponseError";
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
var UsageError = class extends Error {
|
|
43
|
-
constructor(message) {
|
|
44
|
-
super(message);
|
|
45
|
-
this.name = "UsageError";
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
var LocalError = class extends Error {
|
|
49
|
-
constructor(message) {
|
|
50
|
-
super(message);
|
|
51
|
-
this.name = "LocalError";
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
var CommandError = class extends Error {
|
|
55
|
-
constructor(message) {
|
|
56
|
-
super(message);
|
|
57
|
-
this.name = "CommandError";
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// src/core/expert.ts
|
|
62
|
-
var EXPERT_TOKEN_PREFIX = "pxc_";
|
|
63
|
-
var EXPERT_ASSIGNMENTS_PATH = "/api/v1/expert/assignments";
|
|
64
|
-
function isExpertToken(token) {
|
|
65
|
-
return token.startsWith(EXPERT_TOKEN_PREFIX);
|
|
66
|
-
}
|
|
67
|
-
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)";
|
|
68
|
-
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";
|
|
69
|
-
var NOT_AN_EXPERT = "this credential is a member's login or an API token, not an outside expert's pxc_ credential";
|
|
70
|
-
function explainExpertRefusal(error, token) {
|
|
71
|
-
if (!(error instanceof HttpError) || error instanceof UpgradeRequiredError) return error;
|
|
72
|
-
const code = error.body["error"];
|
|
73
|
-
let hint;
|
|
74
|
-
if (error.status === 403 && code === "expert_bound_credential") hint = EXPERT_REACH;
|
|
75
|
-
else if (error.status === 403 && code === "expert_credential_required") hint = NOT_AN_EXPERT;
|
|
76
|
-
else if (error.status === 401 && isExpertToken(token)) hint = EXPERT_LAPSED;
|
|
77
|
-
if (hint === void 0) return error;
|
|
78
|
-
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// src/core/client.ts
|
|
82
|
-
var RECEIVE_TIMEOUT_MS = 3e4;
|
|
83
|
-
var UPLOAD_RECEIVE_TIMEOUT_MS = 6e4;
|
|
84
|
-
var UPLOAD_FLOOR_BYTES_PER_MS = 100;
|
|
85
|
-
function uploadTimeoutMs(bodyBytes) {
|
|
86
|
-
return UPLOAD_RECEIVE_TIMEOUT_MS + Math.ceil(bodyBytes / UPLOAD_FLOOR_BYTES_PER_MS);
|
|
87
|
-
}
|
|
88
|
-
var DEFAULT_MESSAGES = {
|
|
89
|
-
401: "Missing or invalid token (check HARNESS_API_TOKEN)",
|
|
90
|
-
403: "Token not permitted (scope or admin gate)",
|
|
91
|
-
404: "Not found",
|
|
92
|
-
409: "Conflict",
|
|
93
|
-
422: "Validation error",
|
|
94
|
-
426: `CLI too old for this API \u2014 upgrade: npm i -g ${CLI_PACKAGE}@latest`,
|
|
95
|
-
429: "Rate limited \u2014 retry later"
|
|
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
|
-
}
|
|
103
|
-
var Client = class {
|
|
104
|
-
constructor(config) {
|
|
105
|
-
this.config = config;
|
|
106
|
-
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
107
|
-
}
|
|
108
|
-
config;
|
|
109
|
-
fetchImpl;
|
|
110
|
-
get cliVersion() {
|
|
111
|
-
return CLI_VERSION;
|
|
112
|
-
}
|
|
113
|
-
get baseUrl() {
|
|
114
|
-
return this.config.baseUrl;
|
|
115
|
-
}
|
|
116
|
-
async get(path2, params) {
|
|
117
|
-
return this.json(await this.raw("GET", path2, { params }));
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* `actor: false` keeps a configured actor out of the body, for endpoints
|
|
121
|
-
* whose body is closed: they refuse a field they do not name, and record
|
|
122
|
-
* the credential's own person instead.
|
|
123
|
-
*/
|
|
124
|
-
async post(path2, body = {}, options = {}) {
|
|
125
|
-
return this.json(
|
|
126
|
-
await this.raw("POST", path2, {
|
|
127
|
-
json: options.actor === false ? body : this.withActor(body),
|
|
128
|
-
idempotencyKey: options.idempotencyKey
|
|
129
|
-
})
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
async patch(path2, body) {
|
|
133
|
-
return this.json(await this.raw("PATCH", path2, { json: this.withActor(body) }));
|
|
134
|
-
}
|
|
135
|
-
async put(path2, body) {
|
|
136
|
-
return this.json(await this.raw("PUT", path2, { json: this.withActor(body) }));
|
|
137
|
-
}
|
|
138
|
-
async delete(path2) {
|
|
139
|
-
return this.json(await this.raw("DELETE", path2, {}));
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Posts multipart/form-data — the one endpoint that does not take JSON.
|
|
143
|
-
* Shares auth, the version header, and error mapping with the JSON verbs,
|
|
144
|
-
* but never the actor: there is no JSON body to carry an `actor` field,
|
|
145
|
-
* and the server's file contract is the form itself.
|
|
146
|
-
*/
|
|
147
|
-
async postMultipart(path2, form, bodyBytes = 0) {
|
|
148
|
-
return this.json(await this.raw("POST", path2, { form, timeoutMs: uploadTimeoutMs(bodyBytes) }));
|
|
149
|
-
}
|
|
150
|
-
/** Binary download (attachments); non-2xx still maps through the JSON error envelope. */
|
|
151
|
-
async getBytes(path2) {
|
|
152
|
-
const response = await this.raw("GET", path2, {});
|
|
153
|
-
if (!response.ok) throw await this.toHttpError(response);
|
|
154
|
-
try {
|
|
155
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
156
|
-
} catch (error) {
|
|
157
|
-
throw new TransportError(transportMessage(error), transportCode(error));
|
|
158
|
-
}
|
|
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
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* The server's API version, for `version`'s server side. The header rides
|
|
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.
|
|
179
|
-
*/
|
|
180
|
-
async serverVersion() {
|
|
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";
|
|
186
|
-
}
|
|
187
|
-
// Only an explicitly configured actor is sent, and never over one the
|
|
188
|
-
// caller already put in the body.
|
|
189
|
-
withActor(body) {
|
|
190
|
-
if (this.config.actor === null || "actor" in body) return body;
|
|
191
|
-
return { ...body, actor: this.config.actor };
|
|
192
|
-
}
|
|
193
|
-
async raw(method, path2, options) {
|
|
194
|
-
const url = new URL(this.config.baseUrl + path2);
|
|
195
|
-
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
196
|
-
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
197
|
-
}
|
|
198
|
-
const headers = {
|
|
199
|
-
authorization: `Bearer ${this.config.token}`,
|
|
200
|
-
"x-harness-cli-version": CLI_VERSION
|
|
201
|
-
};
|
|
202
|
-
if (options.idempotencyKey !== void 0) {
|
|
203
|
-
headers["idempotency-key"] = options.idempotencyKey;
|
|
204
|
-
}
|
|
205
|
-
let body;
|
|
206
|
-
if (options.json !== void 0) {
|
|
207
|
-
headers["content-type"] = "application/json";
|
|
208
|
-
body = JSON.stringify(options.json);
|
|
209
|
-
} else if (options.form !== void 0) {
|
|
210
|
-
body = options.form;
|
|
211
|
-
}
|
|
212
|
-
try {
|
|
213
|
-
return await this.fetchImpl(url, {
|
|
214
|
-
method,
|
|
215
|
-
headers,
|
|
216
|
-
body,
|
|
217
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? RECEIVE_TIMEOUT_MS),
|
|
218
|
-
redirect: "manual"
|
|
219
|
-
});
|
|
220
|
-
} catch (error) {
|
|
221
|
-
throw new TransportError(transportMessage(error), transportCode(error));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
async json(response) {
|
|
225
|
-
if (!response.ok) throw await this.toHttpError(response);
|
|
226
|
-
let text;
|
|
227
|
-
try {
|
|
228
|
-
text = await response.text();
|
|
229
|
-
} catch (error) {
|
|
230
|
-
throw new TransportError(transportMessage(error), transportCode(error));
|
|
231
|
-
}
|
|
232
|
-
if (text === "") return { data: void 0 };
|
|
233
|
-
let parsed;
|
|
234
|
-
try {
|
|
235
|
-
parsed = JSON.parse(text);
|
|
236
|
-
} catch {
|
|
237
|
-
throw new MalformedResponseError(`the API answered with a non-JSON body (HTTP ${response.status})`);
|
|
238
|
-
}
|
|
239
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
240
|
-
throw new MalformedResponseError(`the API answered with an invalid JSON envelope (HTTP ${response.status})`);
|
|
241
|
-
}
|
|
242
|
-
return parsed;
|
|
243
|
-
}
|
|
244
|
-
async toHttpError(response) {
|
|
245
|
-
let parsed = {};
|
|
246
|
-
try {
|
|
247
|
-
const candidate = JSON.parse(await response.text());
|
|
248
|
-
if (typeof candidate === "object" && candidate !== null) {
|
|
249
|
-
parsed = candidate;
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
}
|
|
253
|
-
const fromBody = typeof parsed["message"] === "string" ? parsed["message"] : null;
|
|
254
|
-
const message = fromBody ?? DEFAULT_MESSAGES[response.status] ?? `HTTP ${response.status}`;
|
|
255
|
-
if (response.status === 426) return new UpgradeRequiredError(message, parsed);
|
|
256
|
-
return new HttpError(response.status, message, parsed, response.headers.get("retry-after"));
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
function transportCode(error) {
|
|
260
|
-
const cause = error instanceof Error ? error.cause : void 0;
|
|
261
|
-
if (typeof cause !== "object" || cause === null) return null;
|
|
262
|
-
const code = cause.code;
|
|
263
|
-
if (typeof code === "string") return code;
|
|
264
|
-
const errors = cause.errors;
|
|
265
|
-
if (!Array.isArray(errors)) return null;
|
|
266
|
-
const codes = new Set(errors.map((inner) => inner?.code));
|
|
267
|
-
const [only] = codes;
|
|
268
|
-
return codes.size === 1 && typeof only === "string" ? only : null;
|
|
269
|
-
}
|
|
270
|
-
function transportMessage(error) {
|
|
271
|
-
if (error instanceof Error) {
|
|
272
|
-
if (error.name === "TimeoutError" || error.name === "AbortError") return "request timed out";
|
|
273
|
-
const cause = error.cause;
|
|
274
|
-
if (cause instanceof Error && cause.message !== "") return cause.message;
|
|
275
|
-
return error.message;
|
|
276
|
-
}
|
|
277
|
-
return String(error);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// src/core/output.ts
|
|
281
|
-
var TITLE_WIDTH = 70;
|
|
282
|
-
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
283
|
-
function graphemes(text) {
|
|
284
|
-
return [...segmenter.segment(text)].map((piece) => piece.segment);
|
|
285
|
-
}
|
|
286
|
-
function graphemeLength(text) {
|
|
287
|
-
return graphemes(text).length;
|
|
288
|
-
}
|
|
289
|
-
function padGraphemes(text, width) {
|
|
290
|
-
const shortfall = width - graphemeLength(text);
|
|
291
|
-
return shortfall > 0 ? text + " ".repeat(shortfall) : text;
|
|
292
|
-
}
|
|
293
|
-
function clean(value) {
|
|
294
|
-
if (value === null || value === void 0) return "";
|
|
295
|
-
return String(value).replace(/[\x00-\x1F\x7F\u0080-\u009F]/gu, "");
|
|
296
|
-
}
|
|
297
|
-
function json(term) {
|
|
298
|
-
return JSON.stringify(term) + "\n";
|
|
299
|
-
}
|
|
300
|
-
var OPERATOR_TEAM_KEY = /^OPERATOR\d*$/;
|
|
301
|
-
var OPERATOR_TEAM_NAME = "Operator";
|
|
302
|
-
var AGENT_MARK = "[agent]";
|
|
303
|
-
var AGENT_LEGEND = `
|
|
304
|
-
${AGENT_MARK} Operator runs this step for its plan; only the plan driver claims it.
|
|
305
|
-
`;
|
|
306
|
-
function isAgentStep(issue) {
|
|
307
|
-
return OPERATOR_TEAM_KEY.test(str(path(issue, "team", "key"))) && str(path(issue, "team", "name")) === OPERATOR_TEAM_NAME;
|
|
308
|
-
}
|
|
309
|
-
function issuesTable(issues, options = {}) {
|
|
310
|
-
if (issues.length === 0) return "No issues.\n";
|
|
311
|
-
let marked = false;
|
|
312
|
-
const rows = issues.map((issue) => {
|
|
313
|
-
const agentStep = options.markAgentSteps === true && isAgentStep(issue);
|
|
314
|
-
if (agentStep) marked = true;
|
|
315
|
-
return [
|
|
316
|
-
str(issue["identifier"]),
|
|
317
|
-
str(path(issue, "state", "name")) || "-",
|
|
318
|
-
str(issue["priority_label"]) || "-",
|
|
319
|
-
str(path(issue, "assignee", "name")) || "-",
|
|
320
|
-
agentStep ? truncate(`${AGENT_MARK} ${clean(issue["title"])}`) : truncate(issue["title"])
|
|
321
|
-
];
|
|
322
|
-
});
|
|
323
|
-
const rendered = table(["ID", "STATE", "PRIORITY", "ASSIGNEE", "TITLE"], rows);
|
|
324
|
-
return marked ? rendered + AGENT_LEGEND : rendered;
|
|
325
|
-
}
|
|
326
|
-
function issueDetail(issue, images = []) {
|
|
327
|
-
const header = `${str(issue["identifier"])} \xB7 ${clean(issue["title"])}
|
|
328
|
-
`;
|
|
329
|
-
const fields = [
|
|
330
|
-
["State", path(issue, "state", "name")],
|
|
331
|
-
["Priority", issue["priority_label"]],
|
|
332
|
-
["Assignee", path(issue, "assignee", "name")],
|
|
333
|
-
["Team", path(issue, "team", "key")],
|
|
334
|
-
["Plan", issuePlanLine(issue["plan"])],
|
|
335
|
-
["Labels", labelsLine(issue["labels"])],
|
|
336
|
-
["Blocked", blockedLine(issue)],
|
|
337
|
-
["Waiting", waitingLine(issue)],
|
|
338
|
-
["Due", issue["due_date"]],
|
|
339
|
-
["Updated", issue["updated_at"]]
|
|
340
|
-
];
|
|
341
|
-
const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
|
|
342
|
-
`).join("");
|
|
343
|
-
const sections = [
|
|
344
|
-
["Description", issue["description"]],
|
|
345
|
-
["Design", issue["design"]],
|
|
346
|
-
["Acceptance criteria", issue["acceptance_criteria"]],
|
|
347
|
-
["Notes", issue["notes"]]
|
|
348
|
-
];
|
|
349
|
-
const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
|
|
350
|
-
${title}:
|
|
351
|
-
${indent(String(text))}
|
|
352
|
-
`).join("");
|
|
353
|
-
return header + fieldLines + sectionText + conditionsSection(issue["conditions"]) + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
|
|
354
|
-
}
|
|
355
|
-
function activityEntries(entries) {
|
|
356
|
-
if (entries.length === 0) return "No activity.\n";
|
|
357
|
-
return entries.map(activityEntry).join("\n");
|
|
358
|
-
}
|
|
359
|
-
function activityEntry(entry) {
|
|
360
|
-
switch (entry["kind"]) {
|
|
361
|
-
case "comment":
|
|
362
|
-
return activityComment(entry);
|
|
363
|
-
case "state_event":
|
|
364
|
-
return activityStateEvent(entry);
|
|
365
|
-
case "assignee_event":
|
|
366
|
-
return activityAssigneeEvent(entry);
|
|
367
|
-
case "condition_event":
|
|
368
|
-
return activityConditionEvent(entry);
|
|
369
|
-
case "field_event":
|
|
370
|
-
return activityFieldEvent(entry);
|
|
371
|
-
default:
|
|
372
|
-
throw new CommandError(`Unknown activity entry kind: ${clean(entry["kind"])}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
function activityComment(entry) {
|
|
376
|
-
const comment2 = requiredRecord(entry["comment"], "activity comment");
|
|
377
|
-
const authorValue = comment2["author"];
|
|
378
|
-
const author = authorValue === null ? "unknown" : optionalLabel(requiredRecord(authorValue, "comment author")["name"], "unknown", "comment author name");
|
|
379
|
-
const deleted = comment2["deleted"];
|
|
380
|
-
if (typeof deleted !== "boolean") throw new CommandError("Invalid comment deleted flag.");
|
|
381
|
-
const body = activityCommentBody(comment2["body"], deleted);
|
|
382
|
-
const text = cleanMultiline(body).split("\n").map((line) => " " + line).join("\n");
|
|
383
|
-
return `[${occurredAt(entry)}] ${clean(author)} (${clean(requiredString(comment2["id"], "comment id"))}):
|
|
384
|
-
${text}
|
|
385
|
-
`;
|
|
386
|
-
}
|
|
387
|
-
function activityCommentBody(value, deleted) {
|
|
388
|
-
if (deleted) return "[deleted comment]";
|
|
389
|
-
if (value === null || value === "") return "[comment has no text]";
|
|
390
|
-
return requiredString(value, "comment body");
|
|
391
|
-
}
|
|
392
|
-
function activityStateEvent(entry) {
|
|
393
|
-
const actor = activityWho(entry);
|
|
394
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
395
|
-
let description;
|
|
396
|
-
switch (entry["action"]) {
|
|
397
|
-
case "created":
|
|
398
|
-
description = `${actor} created the issue in ${stateName(entry["to_state"], "created to_state")}`;
|
|
399
|
-
break;
|
|
400
|
-
case "transition":
|
|
401
|
-
description = `${actor} moved the issue from ${nullableStateName(entry["from_state"], "transition from_state")} to ${stateName(entry["to_state"], "transition to_state")}`;
|
|
402
|
-
break;
|
|
403
|
-
case "milestone":
|
|
404
|
-
description = `${actor} recorded the ${stateName(entry["to_state"], "milestone to_state")} milestone`;
|
|
405
|
-
break;
|
|
406
|
-
default:
|
|
407
|
-
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
408
|
-
}
|
|
409
|
-
return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
|
|
410
|
-
`;
|
|
411
|
-
}
|
|
412
|
-
function activityAssigneeEvent(entry) {
|
|
413
|
-
const actor = activityWho(entry);
|
|
414
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
415
|
-
let description;
|
|
416
|
-
switch (entry["action"]) {
|
|
417
|
-
case "assigned":
|
|
418
|
-
description = `${actor} assigned the issue to ${assigneeName(entry["to_assignee"])}`;
|
|
419
|
-
break;
|
|
420
|
-
case "reassigned":
|
|
421
|
-
description = `${actor} reassigned the issue from ${assigneeName(entry["from_assignee"])} to ${assigneeName(entry["to_assignee"])}`;
|
|
422
|
-
break;
|
|
423
|
-
case "unassigned":
|
|
424
|
-
description = `${actor} unassigned the issue from ${assigneeName(entry["from_assignee"])}`;
|
|
425
|
-
break;
|
|
426
|
-
default:
|
|
427
|
-
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
428
|
-
}
|
|
429
|
-
return `[${occurredAt(entry)}] ${description} - ${provenance}
|
|
430
|
-
`;
|
|
431
|
-
}
|
|
432
|
-
function activityConditionEvent(entry) {
|
|
433
|
-
const actor = activityWho(entry);
|
|
434
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
435
|
-
const condition = conditionSubject(requiredRecord(entry["condition"], "activity condition"));
|
|
436
|
-
let description;
|
|
437
|
-
switch (entry["action"]) {
|
|
438
|
-
case "added":
|
|
439
|
-
description = `${actor} added ${condition}`;
|
|
440
|
-
break;
|
|
441
|
-
case "changed":
|
|
442
|
-
description = `${actor} changed ${condition}`;
|
|
443
|
-
break;
|
|
444
|
-
case "removed":
|
|
445
|
-
description = `${actor} removed ${condition}`;
|
|
446
|
-
break;
|
|
447
|
-
case "satisfied":
|
|
448
|
-
description = `${actor} satisfied ${condition}`;
|
|
449
|
-
break;
|
|
450
|
-
case "unsatisfied":
|
|
451
|
-
description = `${actor} marked ${condition} unsatisfied`;
|
|
452
|
-
break;
|
|
453
|
-
default:
|
|
454
|
-
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
455
|
-
}
|
|
456
|
-
return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
|
|
457
|
-
`;
|
|
458
|
-
}
|
|
459
|
-
function conditionSubject(condition) {
|
|
460
|
-
const label = clean(condition["label"]);
|
|
461
|
-
switch (condition["kind"]) {
|
|
462
|
-
case "gate":
|
|
463
|
-
return label === "" ? "the gate" : `the gate "${label}"`;
|
|
464
|
-
case "not_before": {
|
|
465
|
-
const at = clean(condition["not_before"]);
|
|
466
|
-
return at === "" ? "the not-before date" : `the not-before date ${at}`;
|
|
467
|
-
}
|
|
468
|
-
case "event":
|
|
469
|
-
return label === "" ? "the wait condition" : `the system event "${label}"`;
|
|
470
|
-
default:
|
|
471
|
-
return "the wait condition";
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
function activityFieldEvent(entry) {
|
|
475
|
-
const actor = activityWho(entry);
|
|
476
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
477
|
-
const change = fieldChange(requiredString(entry["field"], "activity field"), entry["from"], entry["to"]);
|
|
478
|
-
return `[${occurredAt(entry)}] ${actor} ${change} \xB7 ${provenance}
|
|
479
|
-
`;
|
|
480
|
-
}
|
|
481
|
-
function fieldChange(field3, from, to) {
|
|
482
|
-
switch (field3) {
|
|
483
|
-
case "priority":
|
|
484
|
-
return `changed priority from ${priorityName(from)} to ${priorityName(to)}`;
|
|
485
|
-
case "title":
|
|
486
|
-
return `renamed the issue from \u201C${activityTitle(from)}\u201D to \u201C${activityTitle(to)}\u201D`;
|
|
487
|
-
case "description":
|
|
488
|
-
return "edited the description";
|
|
489
|
-
case "labels":
|
|
490
|
-
return labelsChange(to);
|
|
491
|
-
case "parent":
|
|
492
|
-
return parentChange(from, to);
|
|
493
|
-
case "archived":
|
|
494
|
-
if (typeof to !== "boolean") throw new CommandError("Invalid activity archived value.");
|
|
495
|
-
return to ? "archived the issue" : "unarchived the issue";
|
|
496
|
-
default:
|
|
497
|
-
return `changed the ${clean(field3)} field`;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
var ACTIVITY_PRIORITIES = {
|
|
501
|
-
0: "No priority",
|
|
502
|
-
1: "Urgent",
|
|
503
|
-
2: "High",
|
|
504
|
-
3: "Medium",
|
|
505
|
-
4: "Low"
|
|
506
|
-
};
|
|
507
|
-
function priorityName(value) {
|
|
508
|
-
if (typeof value !== "number" || !Number.isInteger(value)) throw new CommandError("Invalid activity priority.");
|
|
509
|
-
return ACTIVITY_PRIORITIES[value] ?? String(value);
|
|
510
|
-
}
|
|
511
|
-
function activityTitle(value) {
|
|
512
|
-
if (typeof value !== "string") throw new CommandError("Invalid activity title.");
|
|
513
|
-
return clean(value);
|
|
514
|
-
}
|
|
515
|
-
function labelsChange(to) {
|
|
516
|
-
const change = requiredRecord(to, "activity labels");
|
|
517
|
-
const added = labelNames(change["added"]);
|
|
518
|
-
const removed = labelNames(change["removed"]);
|
|
519
|
-
const parts = [
|
|
520
|
-
...added.length > 0 ? [`added ${labelList(added)}`] : [],
|
|
521
|
-
...removed.length > 0 ? [`removed ${labelList(removed)}`] : []
|
|
522
|
-
];
|
|
523
|
-
return parts.length === 0 ? "changed the labels" : parts.join(" and ");
|
|
524
|
-
}
|
|
525
|
-
function labelNames(value) {
|
|
526
|
-
if (!Array.isArray(value)) throw new CommandError("Invalid activity labels.");
|
|
527
|
-
return value.map((label) => clean(requiredString(requiredRecord(label, "activity labels")["name"], "activity labels")));
|
|
528
|
-
}
|
|
529
|
-
function labelList(names) {
|
|
530
|
-
return `${names.length === 1 ? "label" : "labels"} ${names.join(", ")}`;
|
|
531
|
-
}
|
|
532
|
-
function parentChange(from, to) {
|
|
533
|
-
if (from === null && to === null) return "changed the parent";
|
|
534
|
-
if (from === null) return `set the parent to ${parentName(to)}`;
|
|
535
|
-
if (to === null) return `removed the parent ${parentName(from)}`;
|
|
536
|
-
return `changed the parent from ${parentName(from)} to ${parentName(to)}`;
|
|
537
|
-
}
|
|
538
|
-
function parentName(value) {
|
|
539
|
-
const parent = requiredRecord(value, "activity parent");
|
|
540
|
-
return optionalLabel(parent["identifier"], "an unknown issue", "activity parent identifier");
|
|
541
|
-
}
|
|
542
|
-
function activityWho(entry) {
|
|
543
|
-
const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
|
|
544
|
-
const label = entry["actor_label"];
|
|
545
|
-
if (label === void 0 || label === null || label === "") return actor;
|
|
546
|
-
if (typeof label !== "string") throw new CommandError("Invalid activity actor label.");
|
|
547
|
-
const cleaned = clean(label);
|
|
548
|
-
return cleaned === "" ? actor : `${actor} via \u201C${cleaned}\u201D`;
|
|
549
|
-
}
|
|
550
|
-
function occurredAt(entry) {
|
|
551
|
-
return clean(requiredString(entry["occurred_at"], "activity occurrence time"));
|
|
552
|
-
}
|
|
553
|
-
function assigneeName(value) {
|
|
554
|
-
const assignee = requiredRecord(value, "activity assignee");
|
|
555
|
-
if (assignee["kind"] === "unknown") return "Unknown assignee";
|
|
556
|
-
return activityActor(assignee);
|
|
557
|
-
}
|
|
558
|
-
function activityActor(actor) {
|
|
559
|
-
switch (actor["kind"]) {
|
|
560
|
-
case "user":
|
|
561
|
-
return optionalLabel(actor["name"], "Unknown actor", "activity actor name");
|
|
562
|
-
case "system":
|
|
563
|
-
return "System";
|
|
564
|
-
case "deleted_user":
|
|
565
|
-
return "Deleted user";
|
|
566
|
-
case "unknown":
|
|
567
|
-
return "Unknown actor";
|
|
568
|
-
default:
|
|
569
|
-
throw new CommandError(`Unknown activity actor kind: ${clean(actor["kind"])}`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
function activityProvenance(provenance) {
|
|
573
|
-
switch (provenance["kind"]) {
|
|
574
|
-
case "live":
|
|
575
|
-
case "import":
|
|
576
|
-
case "backfill":
|
|
577
|
-
return String(provenance["kind"]);
|
|
578
|
-
default:
|
|
579
|
-
throw new CommandError(`Unknown activity provenance: ${clean(provenance["kind"])}`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
function stateName(value, field3) {
|
|
583
|
-
return optionalLabel(requiredRecord(value, field3)["name"], "Unknown state", `${field3} name`);
|
|
584
|
-
}
|
|
585
|
-
function nullableStateName(value, field3) {
|
|
586
|
-
return value === null ? "Unknown state" : stateName(value, field3);
|
|
587
|
-
}
|
|
588
|
-
function optionalLabel(value, fallback, field3) {
|
|
589
|
-
if (value === null || value === "") return fallback;
|
|
590
|
-
return clean(requiredString(value, field3));
|
|
591
|
-
}
|
|
592
|
-
function requiredRecord(value, field3) {
|
|
593
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
594
|
-
throw new CommandError(`Invalid ${field3}.`);
|
|
595
|
-
}
|
|
596
|
-
return value;
|
|
597
|
-
}
|
|
598
|
-
function requiredString(value, field3) {
|
|
599
|
-
if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${field3}.`);
|
|
600
|
-
return value;
|
|
601
|
-
}
|
|
602
|
-
function stats(data) {
|
|
603
|
-
const teams = data["teams"] ?? [];
|
|
604
|
-
const totals = data["totals"] ?? {};
|
|
605
|
-
const rows = teams.map((team) => [
|
|
606
|
-
str(path(team, "team", "key")),
|
|
607
|
-
str(team["open"]),
|
|
608
|
-
str(team["started"]),
|
|
609
|
-
str(team["blocked"]),
|
|
610
|
-
str(team["ready"]),
|
|
611
|
-
str(team["closed"])
|
|
612
|
-
]);
|
|
613
|
-
const totalsRow = ["TOTAL", ...["open", "started", "blocked", "ready", "closed"].map((key) => str(totals[key]))];
|
|
614
|
-
return table(["TEAM", "OPEN", "STARTED", "BLOCKED", "READY", "CLOSED"], [...rows, totalsRow]);
|
|
615
|
-
}
|
|
616
|
-
function table(headers, rows) {
|
|
617
|
-
const all = [headers, ...rows.map((row) => row.map((cell) => clean(cell)))];
|
|
618
|
-
const widths = headers.map((_, column) => Math.max(...all.map((row) => graphemeLength(row[column] ?? ""))));
|
|
619
|
-
return all.map(
|
|
620
|
-
(row) => row.map((cell, column) => padGraphemes(cell, widths[column] ?? 0)).join(" ").replace(/\s+$/, "") + "\n"
|
|
621
|
-
).join("");
|
|
622
|
-
}
|
|
623
|
-
function records(value) {
|
|
624
|
-
if (!Array.isArray(value)) return [];
|
|
625
|
-
return value.filter(
|
|
626
|
-
(entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
function waitingLine(issue) {
|
|
630
|
-
if (issue["waiting_on_condition"] !== true) return null;
|
|
631
|
-
const conditions = records(issue["conditions"]);
|
|
632
|
-
const open = conditions.filter((condition) => condition["satisfied"] !== true).length;
|
|
633
|
-
return `yes, on ${open} of ${conditions.length} wait condition${conditions.length === 1 ? "" : "s"}`;
|
|
634
|
-
}
|
|
635
|
-
function conditionsSection(conditions) {
|
|
636
|
-
const list3 = records(conditions);
|
|
637
|
-
if (list3.length === 0) return "";
|
|
638
|
-
return "\nWait conditions:\n" + list3.map((condition) => ` ${conditionLine(condition)}
|
|
639
|
-
`).join("");
|
|
640
|
-
}
|
|
641
|
-
function conditionLine(condition, options = {}) {
|
|
642
|
-
const satisfied = condition["satisfied"] === true;
|
|
643
|
-
let what;
|
|
644
|
-
switch (condition["kind"]) {
|
|
645
|
-
case "gate": {
|
|
646
|
-
const owner = condition["owner"];
|
|
647
|
-
let ownedBy = "";
|
|
648
|
-
if (typeof owner === "object" && owner !== null) {
|
|
649
|
-
const record = owner;
|
|
650
|
-
ownedBy = options.expertView === true && record["kind"] === "expert" ? "you" : ownerText(record);
|
|
651
|
-
}
|
|
652
|
-
what = `gate ${gateName(condition)}${ownedBy === "" ? "" : ` owned by ${ownedBy}`}`;
|
|
653
|
-
break;
|
|
654
|
-
}
|
|
655
|
-
case "not_before":
|
|
656
|
-
what = `not before ${clean(condition["not_before"])}`;
|
|
657
|
-
break;
|
|
658
|
-
case "event":
|
|
659
|
-
what = "system event";
|
|
660
|
-
break;
|
|
661
|
-
default:
|
|
662
|
-
what = clean(condition["kind"]) || "condition";
|
|
663
|
-
}
|
|
664
|
-
let when = "";
|
|
665
|
-
if (satisfied && typeof condition["satisfied_at"] === "string") {
|
|
666
|
-
let by = clean(path(condition, "satisfied_by", "name"));
|
|
667
|
-
if (by === "" && condition["satisfied_by_kind"] === "expert") by = "an outside expert";
|
|
668
|
-
let role = typeof condition["satisfied_as_role"] === "string" ? roleName(condition["satisfied_as_role"]) : "";
|
|
669
|
-
switch (path(condition, "satisfied_by", "kind")) {
|
|
670
|
-
case "you":
|
|
671
|
-
by = "you";
|
|
672
|
-
break;
|
|
673
|
-
case "role": {
|
|
674
|
-
const as = path(condition, "satisfied_by", "role");
|
|
675
|
-
if (typeof as === "string" && as !== "") role = roleName(as);
|
|
676
|
-
break;
|
|
677
|
-
}
|
|
678
|
-
case "expert":
|
|
679
|
-
by = "an outside expert";
|
|
680
|
-
break;
|
|
681
|
-
case "member":
|
|
682
|
-
by = "a member";
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
when = ` (satisfied ${clean(condition["satisfied_at"])}${by === "" ? "" : ` by ${by}`}${role === "" ? "" : ` as ${role}`})`;
|
|
686
|
-
}
|
|
687
|
-
return `${satisfied ? "[x]" : "[ ]"} ${what}${when} \xB7 ${clean(condition["id"])}`;
|
|
688
|
-
}
|
|
689
|
-
function gateName(gate) {
|
|
690
|
-
const key = clean(gate["key"]);
|
|
691
|
-
return `"${clean(gate["label"])}"${key === "" ? "" : ` [${key}]`}`;
|
|
692
|
-
}
|
|
693
|
-
function ownerText(owner) {
|
|
694
|
-
const label = clean(owner["label"]) || (owner["kind"] === "role" && typeof owner["role"] === "string" ? roleName(owner["role"]) : "");
|
|
695
|
-
const ref = clean(owner["ref"]);
|
|
696
|
-
if (owner["kind"] === "expert") {
|
|
697
|
-
const email = label || ref.replace(/^expert:/, "");
|
|
698
|
-
return email === "" ? "" : `${email} (expert)`;
|
|
699
|
-
}
|
|
700
|
-
if (label === "") return ref;
|
|
701
|
-
return ref === "" || ref === label ? label : `${label} (${ref})`;
|
|
702
|
-
}
|
|
703
|
-
function expertSummary(seat) {
|
|
704
|
-
const role = typeof seat.role === "string" && seat.role !== "" ? roleName(seat.role) : "Outside expert";
|
|
705
|
-
const status = clean(seat.plan?.status);
|
|
706
|
-
const plan = seat.plan === null ? ", no plan" : ` on plan ${clean(seat.plan.id)}${status === "" ? "" : ` (${status})`}`;
|
|
707
|
-
const steps = seat.assigned_steps;
|
|
708
|
-
const count = steps === 0 ? "no assigned steps" : `${steps} assigned step${steps === 1 ? "" : "s"}`;
|
|
709
|
-
return `${role}${plan} \xB7 ${count}`;
|
|
710
|
-
}
|
|
711
|
-
function expertAssignments(steps) {
|
|
712
|
-
return steps.map((step) => "\n" + assignedStep(step)).join("");
|
|
713
|
-
}
|
|
714
|
-
function assignedStep(step) {
|
|
715
|
-
const blockers = typeof step["open_blockers"] === "number" ? step["open_blockers"] : 0;
|
|
716
|
-
const fields = [
|
|
717
|
-
["State", path(step, "state", "name")],
|
|
718
|
-
["Due", step["due_date"]],
|
|
719
|
-
["Blocked", blockers > 0 ? `yes, by ${blockers} open issue${blockers === 1 ? "" : "s"}` : null]
|
|
720
|
-
];
|
|
721
|
-
const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
|
|
722
|
-
`).join("");
|
|
723
|
-
const gates = records(step["gates"]);
|
|
724
|
-
const gateLines = gates.length === 0 ? " Gates: none of yours\n" : " Gates:\n" + gates.map((gate) => ` ${conditionLine(gate, { expertView: true })}
|
|
725
|
-
`).join("");
|
|
726
|
-
const description = step["description"];
|
|
727
|
-
const descriptionLines = typeof description === "string" && description !== "" ? " Description:\n" + indent(indent(description)) + "\n" : "";
|
|
728
|
-
return `${clean(step["identifier"])} \xB7 ${clean(step["title"])}
|
|
729
|
-
${fieldLines}${gateLines}${descriptionLines}`;
|
|
730
|
-
}
|
|
731
|
-
var ROLE_NAMES = { gp: "GP" };
|
|
732
|
-
function roleName(role) {
|
|
733
|
-
const slug = clean(role);
|
|
734
|
-
if (Object.hasOwn(ROLE_NAMES, slug)) return ROLE_NAMES[slug];
|
|
735
|
-
const words = slug.replace(/_/g, " ");
|
|
736
|
-
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
737
|
-
}
|
|
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
|
-
}
|
|
767
|
-
function planDetail(plan) {
|
|
768
|
-
const steps = records(plan["steps"]);
|
|
769
|
-
const fields = [
|
|
770
|
-
["Root", planRootLine(plan["root"])],
|
|
771
|
-
["Revision", planRevisionLine(plan)],
|
|
772
|
-
["Budgets", planBudgetsLine(plan)],
|
|
773
|
-
["Estimate", planEstimateLine(plan["estimate"])]
|
|
774
|
-
];
|
|
775
|
-
const fieldLines = fields.filter((field3) => field3[1] !== null).map(([label, value]) => ` ${(label + ":").padEnd(12)}${value}
|
|
776
|
-
`).join("");
|
|
777
|
-
return `Plan ${clean(plan["id"])} \xB7 ${clean(plan["status"])}
|
|
778
|
-
` + fieldLines + planStepsSection(steps, str(plan["status"])) + planEdgesSection(plan["edges"]) + planConditionsSection(steps) + planWaitingSection(steps) + planHeldSection(plan["held_steps"]);
|
|
779
|
-
}
|
|
780
|
-
function planRootLine(root) {
|
|
781
|
-
if (typeof root !== "object" || root === null) return null;
|
|
782
|
-
const record = root;
|
|
783
|
-
return `${clean(record["identifier"])} \xB7 ${clean(record["title"])}`;
|
|
784
|
-
}
|
|
785
|
-
function planRevisionLine(plan) {
|
|
786
|
-
const revision = plan["revision"];
|
|
787
|
-
if (typeof revision !== "object" || revision === null) return "none yet (never released)";
|
|
788
|
-
const number = clean(revision["number"]);
|
|
789
|
-
const by = clean(path(plan, "released_by", "name"));
|
|
790
|
-
const at = clean(plan["released_at"]);
|
|
791
|
-
if (by === "" && at === "") return number;
|
|
792
|
-
return `${number} \xB7 released${by === "" ? "" : ` by ${by}`}${at === "" ? "" : ` at ${at}`}`;
|
|
793
|
-
}
|
|
794
|
-
function planBudgetsLine(plan) {
|
|
795
|
-
const retries = plan["retry_budget"];
|
|
796
|
-
const retryText = typeof retries === "number" ? `${retries} ${retries === 1 ? "retry" : "retries"} per failed Operator step` : null;
|
|
797
|
-
const spend = plan["spend_budget_cents"];
|
|
798
|
-
const spendText = typeof spend === "number" ? `spend budget ${spend} cents` : "no spend budget";
|
|
799
|
-
return [retryText, spendText].filter((part) => part !== null).join(" \xB7 ");
|
|
800
|
-
}
|
|
801
|
-
function planEstimateLine(estimate) {
|
|
802
|
-
if (typeof estimate !== "object" || estimate === null) return null;
|
|
803
|
-
const record = estimate;
|
|
804
|
-
const ceiling = record["ceiling_cents"];
|
|
805
|
-
if (typeof ceiling !== "number") return null;
|
|
806
|
-
const minimum = record["minimum_budget_cents"];
|
|
807
|
-
return `up to ${ceiling} cents${typeof minimum === "number" ? ` (minimum budget ${minimum} cents)` : ""}`;
|
|
808
|
-
}
|
|
809
|
-
function planStepsSection(steps, planStatus) {
|
|
810
|
-
if (steps.length === 0) return "";
|
|
811
|
-
const depths = stepDepths(steps);
|
|
812
|
-
const rows = steps.map((step) => [
|
|
813
|
-
" ".repeat(depths.get(str(step["identifier"])) ?? 0) + str(step["identifier"]),
|
|
814
|
-
str(step["key"]) || "-",
|
|
815
|
-
str(step["executor"]) || "-",
|
|
816
|
-
str(path(step, "state", "name")) || "-",
|
|
817
|
-
stepStatus(step, planStatus),
|
|
818
|
-
truncate(step["title"])
|
|
819
|
-
]);
|
|
820
|
-
const rendered = table(["STEP", "KEY", "EXECUTOR", "STATE", "STATUS", "TITLE"], rows);
|
|
821
|
-
return "\nSteps:\n" + indentLines(rendered);
|
|
822
|
-
}
|
|
823
|
-
function stepDepths(steps) {
|
|
824
|
-
const parents = new Map(steps.map((step) => [str(step["identifier"]), str(step["parent"])]));
|
|
825
|
-
const depths = /* @__PURE__ */ new Map();
|
|
826
|
-
for (const identifier of parents.keys()) {
|
|
827
|
-
let depth = 0;
|
|
828
|
-
let parent = parents.get(identifier) ?? "";
|
|
829
|
-
const seen = /* @__PURE__ */ new Set([identifier]);
|
|
830
|
-
while (parent !== "" && parents.has(parent) && !seen.has(parent)) {
|
|
831
|
-
seen.add(parent);
|
|
832
|
-
depth += 1;
|
|
833
|
-
parent = parents.get(parent) ?? "";
|
|
834
|
-
}
|
|
835
|
-
depths.set(identifier, depth);
|
|
836
|
-
}
|
|
837
|
-
return depths;
|
|
838
|
-
}
|
|
839
|
-
function stepStatus(step, planStatus) {
|
|
840
|
-
const stateType = path(step, "state", "type");
|
|
841
|
-
if (step["archived"] === true) return "archived";
|
|
842
|
-
if (stateType === "completed") return "done";
|
|
843
|
-
if (stateType === "canceled") return "cancelled";
|
|
844
|
-
if (step["container"] === true) return "container";
|
|
845
|
-
if (step["ready"] === true) return "ready";
|
|
846
|
-
if (step["held"] === true) return "held";
|
|
847
|
-
if (step["in_revision"] === false && LIVE_PLAN_STATUSES.includes(planStatus)) return "not in revision";
|
|
848
|
-
if (records(step["conditions"]).some((condition) => condition["satisfied"] !== true)) return "waiting";
|
|
849
|
-
return "-";
|
|
850
|
-
}
|
|
851
|
-
function planEdgesSection(edges) {
|
|
852
|
-
const list3 = records(edges);
|
|
853
|
-
if (list3.length === 0) return "";
|
|
854
|
-
return "\nEdges (blocker \u2192 blocked):\n" + list3.map((edge) => ` ${clean(edge["from"])} \u2192 ${clean(edge["to"])}
|
|
855
|
-
`).join("");
|
|
856
|
-
}
|
|
857
|
-
function planConditionsSection(steps) {
|
|
858
|
-
const lines = steps.flatMap(
|
|
859
|
-
(step) => records(step["conditions"]).map((condition) => [clean(step["identifier"]), conditionLine(condition)])
|
|
860
|
-
);
|
|
861
|
-
if (lines.length === 0) return "";
|
|
862
|
-
const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
|
|
863
|
-
return "\nWait conditions:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
|
|
864
|
-
`).join("");
|
|
865
|
-
}
|
|
866
|
-
function planWaitingSection(steps) {
|
|
867
|
-
const lines = steps.flatMap(
|
|
868
|
-
(step) => records(step["waiting_for"]).map((wait) => [clean(step["identifier"]), waitLine(wait)])
|
|
869
|
-
);
|
|
870
|
-
if (lines.length === 0) return "";
|
|
871
|
-
const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
|
|
872
|
-
return "\nWaiting for:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
|
|
873
|
-
`).join("");
|
|
874
|
-
}
|
|
875
|
-
function waitLine(wait) {
|
|
876
|
-
const name = gateName(wait);
|
|
877
|
-
const owner = wait["owner"];
|
|
878
|
-
if (typeof owner !== "object" || owner === null) return `${name} \xB7 no owner, anyone with standing can tick`;
|
|
879
|
-
const holders = wait["holders"];
|
|
880
|
-
let who;
|
|
881
|
-
if (!Array.isArray(holders)) who = "who can tick is not known";
|
|
882
|
-
else if (holders.length === 0) who = "nobody can tick";
|
|
883
|
-
else who = `can tick: ${records(holders).map(holderText).join(", ")}`;
|
|
884
|
-
return `${name} \xB7 ${ownerText(owner)} \xB7 ${who}`;
|
|
885
|
-
}
|
|
886
|
-
function holderText(holder) {
|
|
887
|
-
const name = clean(holder["name"]);
|
|
888
|
-
if (holder["kind"] !== "expert") return name;
|
|
889
|
-
const organization = clean(holder["organization"]);
|
|
890
|
-
return `${name} (expert${organization === "" ? "" : `, ${organization}`})`;
|
|
891
|
-
}
|
|
892
|
-
function planHeldSection(heldSteps) {
|
|
893
|
-
if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
|
|
894
|
-
return `
|
|
895
|
-
Changed since the release: ${heldSteps.map((step) => clean(step)).join(", ")} (only Operator steps are held until the next release takes them in)
|
|
896
|
-
`;
|
|
897
|
-
}
|
|
898
|
-
function indentLines(text) {
|
|
899
|
-
return text.split("\n").map((line) => line === "" ? line : " " + line).join("\n");
|
|
900
|
-
}
|
|
901
|
-
function errorDetails(body) {
|
|
902
|
-
return changesetDetails(body["details"]) + violationDetails(body["violations"]);
|
|
903
|
-
}
|
|
904
|
-
function changesetDetails(details) {
|
|
905
|
-
if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
|
|
906
|
-
return Object.entries(details).map(([field3, messages]) => {
|
|
907
|
-
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
908
|
-
return `
|
|
909
|
-
${clean(field3)}: ${clean(text)}`;
|
|
910
|
-
}).join("");
|
|
911
|
-
}
|
|
912
|
-
function violationDetails(violations) {
|
|
913
|
-
return records(violations).map((violation) => {
|
|
914
|
-
const code = clean(violation["code"]);
|
|
915
|
-
const message = capMessage(violation) ?? (clean(violation["message"]) || code || "invalid");
|
|
916
|
-
return `
|
|
917
|
-
${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
|
|
918
|
-
}).join("");
|
|
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
|
-
}
|
|
941
|
-
function violationSubject(violation) {
|
|
942
|
-
const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
|
|
943
|
-
const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
|
|
944
|
-
const issue = typeof violation["issue"] === "string" ? clean(violation["issue"]) : null;
|
|
945
|
-
if (key !== null) {
|
|
946
|
-
const where = index ?? issue;
|
|
947
|
-
return where === null ? `step "${key}"` : `step "${key}" (${where})`;
|
|
948
|
-
}
|
|
949
|
-
return issue ?? index ?? "plan";
|
|
950
|
-
}
|
|
951
|
-
function relationsSection(relations) {
|
|
952
|
-
if (!Array.isArray(relations) || relations.length === 0) return "";
|
|
953
|
-
const lines = relations.map((relation) => ` ${str(relation["direction"]).padEnd(14)}${str(relation["other_issue"])}
|
|
954
|
-
`).join("");
|
|
955
|
-
return "\nRelations:\n" + lines;
|
|
956
|
-
}
|
|
957
|
-
function childrenSection(children) {
|
|
958
|
-
if (!Array.isArray(children) || children.length === 0) return "";
|
|
959
|
-
const lines = children.map((child) => {
|
|
960
|
-
const state = str(path(child, "state", "name")) || "-";
|
|
961
|
-
return ` ${str(child["identifier"])} [${state}] ${truncate(child["title"])}
|
|
962
|
-
`;
|
|
963
|
-
}).join("");
|
|
964
|
-
return "\nSub-issues:\n" + lines;
|
|
965
|
-
}
|
|
966
|
-
function commentsSection(comments2) {
|
|
967
|
-
if (!Array.isArray(comments2) || comments2.length === 0) return "";
|
|
968
|
-
const lines = comments2.map((comment2) => {
|
|
969
|
-
const author = clean(path(comment2, "author", "name") ?? "unknown");
|
|
970
|
-
return ` [${str(comment2["inserted_at"])}] ${author}: ${truncate(comment2["body"])}
|
|
971
|
-
`;
|
|
972
|
-
}).join("");
|
|
973
|
-
return "\nComments:\n" + lines;
|
|
974
|
-
}
|
|
975
|
-
function imagesSection(images) {
|
|
976
|
-
if (images.length === 0) return "";
|
|
977
|
-
const lines = images.map((image) => ` [${image["source"]}] ${clean(image["path"])}
|
|
978
|
-
`).join("");
|
|
979
|
-
return "\nImages (attachments downloaded locally \u2014 read them, they are part of the issue):\n" + lines;
|
|
980
|
-
}
|
|
981
|
-
function labelsLine(labels) {
|
|
982
|
-
if (!Array.isArray(labels) || labels.length === 0) return null;
|
|
983
|
-
return labels.map((label) => clean(label["name"])).join(", ");
|
|
984
|
-
}
|
|
985
|
-
function blockedLine(issue) {
|
|
986
|
-
if (issue["blocked"] === true && Array.isArray(issue["blocked_by"])) {
|
|
987
|
-
const notes = blockerNotes(issue["blockers"]);
|
|
988
|
-
const blockers = issue["blocked_by"].map((blocker) => {
|
|
989
|
-
const identifier = String(blocker);
|
|
990
|
-
const note = notes.get(identifier);
|
|
991
|
-
return note === void 0 ? identifier : `${identifier} (${note})`;
|
|
992
|
-
});
|
|
993
|
-
return `yes, by ${blockers.join(", ")}`;
|
|
994
|
-
}
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
function blockerNotes(blockers) {
|
|
998
|
-
const notes = /* @__PURE__ */ new Map();
|
|
999
|
-
if (!Array.isArray(blockers)) return notes;
|
|
1000
|
-
for (const blocker of blockers) {
|
|
1001
|
-
if (typeof blocker !== "object" || blocker === null) continue;
|
|
1002
|
-
const record = blocker;
|
|
1003
|
-
if (typeof record["identifier"] !== "string") continue;
|
|
1004
|
-
const reasons = [];
|
|
1005
|
-
if (record["state_type"] === "canceled") reasons.push("cancelled");
|
|
1006
|
-
if (record["archived"] === true) reasons.push("archived");
|
|
1007
|
-
if (reasons.length > 0) notes.set(record["identifier"], reasons.join(", "));
|
|
1008
|
-
}
|
|
1009
|
-
return notes;
|
|
1010
|
-
}
|
|
1011
|
-
function indent(text) {
|
|
1012
|
-
return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
|
|
1013
|
-
}
|
|
1014
|
-
function cleanMultiline(text) {
|
|
1015
|
-
return text.split("\n").map((line) => clean(line)).join("\n");
|
|
1016
|
-
}
|
|
1017
|
-
function truncate(text) {
|
|
1018
|
-
if (text === null || text === void 0) return "";
|
|
1019
|
-
const cleaned = clean(text);
|
|
1020
|
-
const pieces = graphemes(cleaned);
|
|
1021
|
-
if (pieces.length > TITLE_WIDTH) {
|
|
1022
|
-
return pieces.slice(0, TITLE_WIDTH - 1).join("") + "\u2026";
|
|
1023
|
-
}
|
|
1024
|
-
return cleaned;
|
|
1025
|
-
}
|
|
1026
|
-
function str(value) {
|
|
1027
|
-
return value === null || value === void 0 ? "" : String(value);
|
|
1028
|
-
}
|
|
1029
|
-
function path(record, ...keys) {
|
|
1030
|
-
let current = record;
|
|
1031
|
-
for (const key of keys) {
|
|
1032
|
-
if (typeof current !== "object" || current === null) return null;
|
|
1033
|
-
current = current[key];
|
|
1034
|
-
}
|
|
1035
|
-
return current ?? null;
|
|
1036
|
-
}
|
|
37
|
+
} from "./chunk-5THYIKLW.js";
|
|
1037
38
|
|
|
1038
39
|
// src/commands/issues.ts
|
|
1039
40
|
import { openAsBlob, openSync, closeSync, readFileSync, statSync as statSync2 } from "fs";
|
|
@@ -1101,14 +102,14 @@ async function fetchImages(client, issue, cacheDir) {
|
|
|
1101
102
|
return { images, failures };
|
|
1102
103
|
}
|
|
1103
104
|
async function download(client, ref, cacheDir) {
|
|
1104
|
-
const
|
|
105
|
+
const path = join(cacheDir, hostSegment(client.baseUrl), ref.attachmentId, ref.file);
|
|
1105
106
|
try {
|
|
1106
|
-
if (statSync(
|
|
107
|
+
if (statSync(path).size > 0) return { ok: true, path };
|
|
1107
108
|
} catch {
|
|
1108
109
|
}
|
|
1109
110
|
const bytes = await fetchBytes(client, ref);
|
|
1110
111
|
if (!bytes.ok) return bytes;
|
|
1111
|
-
return write(
|
|
112
|
+
return write(path, bytes.bytes);
|
|
1112
113
|
}
|
|
1113
114
|
function hostSegment(baseUrl) {
|
|
1114
115
|
let host = "unknown-host";
|
|
@@ -1148,25 +149,25 @@ function isImage(bytes) {
|
|
|
1148
149
|
return false;
|
|
1149
150
|
}
|
|
1150
151
|
var partialCounter = 0;
|
|
1151
|
-
function write(
|
|
1152
|
-
const partial = `${
|
|
152
|
+
function write(path, bytes) {
|
|
153
|
+
const partial = `${path}.partial.${process.pid}.${++partialCounter}`;
|
|
1153
154
|
try {
|
|
1154
|
-
mkdirSync(dirname(
|
|
1155
|
-
chmodSync(dirname(
|
|
155
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
156
|
+
chmodSync(dirname(path), 448);
|
|
1156
157
|
writeFileSync(partial, bytes);
|
|
1157
158
|
chmodSync(partial, 384);
|
|
1158
|
-
renameSync(partial,
|
|
1159
|
-
return { ok: true, path
|
|
159
|
+
renameSync(partial, path);
|
|
160
|
+
return { ok: true, path };
|
|
1160
161
|
} catch (error) {
|
|
1161
162
|
rmSync(partial, { force: true });
|
|
1162
163
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1163
|
-
return { ok: false, message: `cannot write ${
|
|
164
|
+
return { ok: false, message: `cannot write ${path}: ${detail}` };
|
|
1164
165
|
}
|
|
1165
166
|
}
|
|
1166
167
|
|
|
1167
168
|
// src/core/pagination.ts
|
|
1168
|
-
function notices(meta, requestedPageSize,
|
|
1169
|
-
return capNotice(meta, requestedPageSize) + truncationNotice(meta,
|
|
169
|
+
function notices(meta, requestedPageSize, records4) {
|
|
170
|
+
return capNotice(meta, requestedPageSize) + truncationNotice(meta, records4);
|
|
1170
171
|
}
|
|
1171
172
|
function capNotice(meta, requestedPageSize) {
|
|
1172
173
|
const served = meta?.page_size;
|
|
@@ -1176,12 +177,12 @@ function capNotice(meta, requestedPageSize) {
|
|
|
1176
177
|
}
|
|
1177
178
|
return "";
|
|
1178
179
|
}
|
|
1179
|
-
function truncationNotice(meta,
|
|
180
|
+
function truncationNotice(meta, records4) {
|
|
1180
181
|
const total = meta?.total;
|
|
1181
182
|
const page = meta?.page;
|
|
1182
183
|
const pages = meta?.total_pages;
|
|
1183
184
|
if (typeof total !== "number" || typeof page !== "number" || typeof pages !== "number") return "";
|
|
1184
|
-
const shown =
|
|
185
|
+
const shown = records4.length;
|
|
1185
186
|
if (shown < total) {
|
|
1186
187
|
return `harness: showing ${shown} of ${total} (page ${page} of ${pages}) \u2014 pass --all for the rest, or --page to step through.
|
|
1187
188
|
`;
|
|
@@ -1192,20 +193,20 @@ async function fetchEveryPage(fetchPage) {
|
|
|
1192
193
|
const first = await fetchPage(1);
|
|
1193
194
|
const totalPages = pageCount(first.meta);
|
|
1194
195
|
if (totalPages === null) return first.data;
|
|
1195
|
-
let
|
|
196
|
+
let records4 = first.data;
|
|
1196
197
|
for (let page = 2; page <= totalPages; page++) {
|
|
1197
198
|
const body = await fetchPage(page);
|
|
1198
|
-
|
|
199
|
+
records4 = dedupe([...records4, ...body.data]);
|
|
1199
200
|
}
|
|
1200
|
-
return
|
|
201
|
+
return records4;
|
|
1201
202
|
}
|
|
1202
203
|
function pageCount(meta) {
|
|
1203
204
|
const total = meta?.total_pages;
|
|
1204
205
|
return typeof total === "number" && total > 0 ? total : null;
|
|
1205
206
|
}
|
|
1206
|
-
function dedupe(
|
|
207
|
+
function dedupe(records4) {
|
|
1207
208
|
const seen = /* @__PURE__ */ new Set();
|
|
1208
|
-
return
|
|
209
|
+
return records4.filter((record) => {
|
|
1209
210
|
const id = record["id"];
|
|
1210
211
|
const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
|
|
1211
212
|
if (seen.has(key)) return false;
|
|
@@ -1247,9 +248,9 @@ function requireIdentifiers(identifiers) {
|
|
|
1247
248
|
async function ready(ctx, opts) {
|
|
1248
249
|
if (opts.claim === true) return claimNext(ctx, opts);
|
|
1249
250
|
const params = { ...listParams(ctx, opts), ready: true };
|
|
1250
|
-
const { records:
|
|
251
|
+
const { records: records4, notice } = await fetchIssues(ctx, params, opts);
|
|
1251
252
|
const rank = (issue) => PRIORITY_RANK[issue["priority"]] ?? 4;
|
|
1252
|
-
const sorted = markAgentSteps([...
|
|
253
|
+
const sorted = markAgentSteps([...records4].sort((a, b) => rank(a) - rank(b)));
|
|
1253
254
|
return { json: sorted, human: issuesTable(sorted, { markAgentSteps: true }), notice };
|
|
1254
255
|
}
|
|
1255
256
|
function markAgentSteps(issues) {
|
|
@@ -1293,7 +294,7 @@ async function claimNext(ctx, opts) {
|
|
|
1293
294
|
if (!isIssueRecord(claimed)) throw unreadable("carried neither an issue nor null");
|
|
1294
295
|
return {
|
|
1295
296
|
json: claimed,
|
|
1296
|
-
human: `Claimed ${
|
|
297
|
+
human: `Claimed ${str(claimed["identifier"])}: ${clean(claimed["title"])}
|
|
1297
298
|
`
|
|
1298
299
|
};
|
|
1299
300
|
}
|
|
@@ -1304,13 +305,13 @@ function isIssueRecord(value) {
|
|
|
1304
305
|
}
|
|
1305
306
|
async function blocked(ctx, opts) {
|
|
1306
307
|
const params = { ...listParams(ctx, opts), blocked: true };
|
|
1307
|
-
const { records:
|
|
1308
|
-
return { json:
|
|
308
|
+
const { records: records4, notice } = await fetchIssues(ctx, params, opts);
|
|
309
|
+
return { json: records4, human: issuesTable(records4), notice };
|
|
1309
310
|
}
|
|
1310
311
|
async function list(ctx, opts) {
|
|
1311
312
|
const params = listParams(ctx, opts);
|
|
1312
|
-
const { records:
|
|
1313
|
-
return { json:
|
|
313
|
+
const { records: records4, notice } = await fetchIssues(ctx, params, opts);
|
|
314
|
+
return { json: records4, human: issuesTable(records4), notice };
|
|
1314
315
|
}
|
|
1315
316
|
async function search(ctx, queryWords, opts) {
|
|
1316
317
|
if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
|
|
@@ -1334,11 +335,11 @@ async function show(ctx, identifier, opts) {
|
|
|
1334
335
|
if (opts.images !== false) {
|
|
1335
336
|
({ images, failures } = await fetchImages(ctx.client, issue, ctx.config.cacheDir));
|
|
1336
337
|
}
|
|
1337
|
-
const
|
|
338
|
+
const json = images.length === 0 ? issue : { ...issue, local_images: images };
|
|
1338
339
|
const human = issueDetail(issue, images);
|
|
1339
340
|
const notice = failures.map((failure) => `harness: image not downloaded: ${clean(failure)}
|
|
1340
341
|
`).join("");
|
|
1341
|
-
return notice === "" ? { json
|
|
342
|
+
return notice === "" ? { json, human } : { json, human, notice };
|
|
1342
343
|
}
|
|
1343
344
|
var ACTIVITY_MAX_LIMIT = 100;
|
|
1344
345
|
var ACTIVITY_INCLUDE = "condition_events,field_events";
|
|
@@ -1428,7 +429,7 @@ async function stats2(ctx) {
|
|
|
1428
429
|
async function prime(ctx) {
|
|
1429
430
|
const statsBody = await ctx.client.get("/api/v1/stats");
|
|
1430
431
|
const ready2 = markAgentSteps(await mostImportantReady(ctx));
|
|
1431
|
-
const
|
|
432
|
+
const json = { stats: statsBody.data, ready: ready2 };
|
|
1432
433
|
const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + named(`
|
|
1433
434
|
Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
|
|
1434
435
|
harness close <id> --reason "Implemented in <commit-hash>" (response
|
|
@@ -1437,7 +438,7 @@ Create tracked work BEFORE coding: harness create --title "\u2026" --type task.
|
|
|
1437
438
|
Dependencies: harness dep add <issue> <blocker> (blocker blocks issue).
|
|
1438
439
|
Before reporting done: close the issues you finished.
|
|
1439
440
|
`);
|
|
1440
|
-
return { json
|
|
441
|
+
return { json, human };
|
|
1441
442
|
}
|
|
1442
443
|
async function mostImportantReady(ctx) {
|
|
1443
444
|
try {
|
|
@@ -1484,7 +485,7 @@ async function create(ctx, opts) {
|
|
|
1484
485
|
const issue = created.data;
|
|
1485
486
|
return {
|
|
1486
487
|
json: issue,
|
|
1487
|
-
human: `Created ${
|
|
488
|
+
human: `Created ${str(issue["identifier"])}: ${clean(issue["title"])}
|
|
1488
489
|
`
|
|
1489
490
|
};
|
|
1490
491
|
}
|
|
@@ -1537,7 +538,7 @@ async function update(ctx, identifier, opts) {
|
|
|
1537
538
|
if (opts.appendNotes !== void 0) {
|
|
1538
539
|
const current = await ctx.client.get(`/api/v1/issues/${identifier}`);
|
|
1539
540
|
const existing = current.data["notes"];
|
|
1540
|
-
const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${
|
|
541
|
+
const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${str(existing)}
|
|
1541
542
|
|
|
1542
543
|
${opts.appendNotes}`;
|
|
1543
544
|
patch["notes"] = combined;
|
|
@@ -1561,8 +562,8 @@ ${opts.appendNotes}`;
|
|
|
1561
562
|
if (issue === null) {
|
|
1562
563
|
throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
|
|
1563
564
|
}
|
|
1564
|
-
const human = wrote ? `Updated ${
|
|
1565
|
-
` : `Labels already up to date for ${
|
|
565
|
+
const human = wrote ? `Updated ${str(issue["identifier"])}
|
|
566
|
+
` : `Labels already up to date for ${str(issue["identifier"])}
|
|
1566
567
|
`;
|
|
1567
568
|
return { json: issue, human };
|
|
1568
569
|
}
|
|
@@ -1639,7 +640,7 @@ async function close(ctx, identifiers, opts) {
|
|
|
1639
640
|
}
|
|
1640
641
|
}
|
|
1641
642
|
const human = results.map((result) => closeLine(result, opts.duplicateOf)).join("");
|
|
1642
|
-
const
|
|
643
|
+
const json = results.map(
|
|
1643
644
|
(result) => result.ok ? {
|
|
1644
645
|
identifier: result.identifier,
|
|
1645
646
|
closed: true,
|
|
@@ -1650,7 +651,7 @@ async function close(ctx, identifiers, opts) {
|
|
|
1650
651
|
} : { identifier: result.identifier, closed: false, error: errorText(result.error) }
|
|
1651
652
|
);
|
|
1652
653
|
const partial = results.some((result) => !result.ok);
|
|
1653
|
-
return partial ? { json
|
|
654
|
+
return partial ? { json, human, partial: true } : { json, human };
|
|
1654
655
|
}
|
|
1655
656
|
function closeLine(result, duplicateOf) {
|
|
1656
657
|
if (!result.ok) return `FAILED ${result.identifier}: ${errorText(result.error)}
|
|
@@ -1693,25 +694,25 @@ async function archive(ctx, identifiers, opts) {
|
|
|
1693
694
|
` : `FAILED ${result.identifier}: ${errorText(result.error)}
|
|
1694
695
|
`
|
|
1695
696
|
).join("");
|
|
1696
|
-
const
|
|
697
|
+
const json = results.map(
|
|
1697
698
|
(result) => result.ok ? { identifier: result.identifier, archived } : { identifier: result.identifier, error: errorText(result.error) }
|
|
1698
699
|
);
|
|
1699
700
|
const partial = results.some((result) => !result.ok);
|
|
1700
|
-
return partial ? { json
|
|
701
|
+
return partial ? { json, human, partial: true } : { json, human };
|
|
1701
702
|
}
|
|
1702
703
|
async function comments(ctx, identifier) {
|
|
1703
704
|
requireIdentifiers([identifier]);
|
|
1704
|
-
const
|
|
1705
|
-
if (
|
|
1706
|
-
const human =
|
|
705
|
+
const records4 = await completeCommentThread(ctx, identifier);
|
|
706
|
+
if (records4.length === 0) return { json: records4, human: "No comments.\n" };
|
|
707
|
+
const human = records4.map((comment2) => {
|
|
1707
708
|
const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
|
|
1708
709
|
const id = clean(comment2["id"] ?? "unknown id");
|
|
1709
|
-
const text = cleanMultiline(
|
|
1710
|
-
return `[${
|
|
710
|
+
const text = cleanMultiline(str(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
|
|
711
|
+
return `[${str(comment2["inserted_at"])}] ${author} (${id}):
|
|
1711
712
|
${text}
|
|
1712
713
|
`;
|
|
1713
714
|
}).join("\n");
|
|
1714
|
-
return { json:
|
|
715
|
+
return { json: records4, human };
|
|
1715
716
|
}
|
|
1716
717
|
async function completeCommentThread(ctx, identifier) {
|
|
1717
718
|
const pages = [];
|
|
@@ -1725,20 +726,20 @@ async function completeCommentThread(ctx, identifier) {
|
|
|
1725
726
|
if (!Array.isArray(page.data) || typeof page.meta?.has_more !== "boolean") {
|
|
1726
727
|
throw new CommandError("Invalid comment pagination: expected a comment array and boolean meta.has_more; completeness unknown.");
|
|
1727
728
|
}
|
|
1728
|
-
const
|
|
729
|
+
const records4 = [];
|
|
1729
730
|
for (const record of page.data) {
|
|
1730
731
|
if (record === null || typeof record !== "object" || Array.isArray(record) || typeof record.id !== "string" || record.id.trim() === "" || seen.has(record.id)) {
|
|
1731
732
|
throw new CommandError("Invalid comment pagination: missing or repeated comment id; completeness unknown.");
|
|
1732
733
|
}
|
|
1733
734
|
seen.add(record.id);
|
|
1734
|
-
|
|
735
|
+
records4.push(record);
|
|
1735
736
|
}
|
|
1736
|
-
pages.push(
|
|
737
|
+
pages.push(records4);
|
|
1737
738
|
if (!page.meta.has_more) return pages.reverse().flat();
|
|
1738
|
-
if (
|
|
739
|
+
if (records4.length === 0) {
|
|
1739
740
|
throw new CommandError("Invalid comment pagination: has_more on an empty page; completeness unknown.");
|
|
1740
741
|
}
|
|
1741
|
-
before =
|
|
742
|
+
before = records4[0]["id"];
|
|
1742
743
|
}
|
|
1743
744
|
}
|
|
1744
745
|
async function dep(ctx, verb, issue, blocker) {
|
|
@@ -1757,7 +758,7 @@ async function dep(ctx, verb, issue, blocker) {
|
|
|
1757
758
|
(candidate) => candidate["direction"] === "blocked_by" && candidate["other_issue"] === blocker
|
|
1758
759
|
);
|
|
1759
760
|
if (relation === void 0) throw new CommandError(`${blocker} does not block this issue.`);
|
|
1760
|
-
await ctx.client.delete(`/api/v1/issues/${issue}/relations/${
|
|
761
|
+
await ctx.client.delete(`/api/v1/issues/${issue}/relations/${str(relation["id"])}`);
|
|
1761
762
|
return { json: { deleted: relation["id"] }, human: `${blocker} no longer blocks ${issue}
|
|
1762
763
|
` };
|
|
1763
764
|
}
|
|
@@ -1880,17 +881,17 @@ function requireCommentId(commentId) {
|
|
|
1880
881
|
}
|
|
1881
882
|
var MAX_IMAGE_BYTES = 25e6;
|
|
1882
883
|
var MAX_VIDEO_BYTES = 1e8;
|
|
1883
|
-
async function attachImage(ctx,
|
|
1884
|
-
return uploadAttachment(ctx,
|
|
884
|
+
async function attachImage(ctx, path) {
|
|
885
|
+
return uploadAttachment(ctx, path, MAX_IMAGE_BYTES);
|
|
1885
886
|
}
|
|
1886
|
-
async function attachFile(ctx,
|
|
1887
|
-
const maxBytes = extname(
|
|
1888
|
-
return uploadAttachment(ctx,
|
|
887
|
+
async function attachFile(ctx, path) {
|
|
888
|
+
const maxBytes = extname(path).toLowerCase() === ".mp4" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
|
|
889
|
+
return uploadAttachment(ctx, path, maxBytes);
|
|
1889
890
|
}
|
|
1890
|
-
async function uploadAttachment(ctx,
|
|
1891
|
-
attachableFile(
|
|
1892
|
-
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(
|
|
1893
|
-
requireAttachmentSize(
|
|
891
|
+
async function uploadAttachment(ctx, path, maxBytes) {
|
|
892
|
+
attachableFile(path, maxBytes);
|
|
893
|
+
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path)], basename(path)) : new File([readFileSync(path)], basename(path));
|
|
894
|
+
requireAttachmentSize(path, file.size, maxBytes);
|
|
1894
895
|
const form = new FormData();
|
|
1895
896
|
form.set("file", file);
|
|
1896
897
|
const body = await ctx.client.postMultipart("/api/v1/uploads", form, file.size);
|
|
@@ -1899,27 +900,27 @@ async function uploadAttachment(ctx, path2, maxBytes) {
|
|
|
1899
900
|
if (typeof markdown !== "string") throw new CommandError("the upload response carried no markdown reference");
|
|
1900
901
|
return { json: attachment, human: clean(markdown) + "\n" };
|
|
1901
902
|
}
|
|
1902
|
-
function attachableFile(
|
|
903
|
+
function attachableFile(path, maxBytes) {
|
|
1903
904
|
let fileStats;
|
|
1904
905
|
try {
|
|
1905
|
-
fileStats = statSync2(
|
|
906
|
+
fileStats = statSync2(path);
|
|
1906
907
|
} catch (error) {
|
|
1907
|
-
throw new LocalError(`cannot read ${
|
|
908
|
+
throw new LocalError(`cannot read ${path}: ${posixDetail(error)}`);
|
|
1908
909
|
}
|
|
1909
910
|
if (!fileStats.isFile()) {
|
|
1910
911
|
const kind = fileStats.isDirectory() ? "directory" : "special file";
|
|
1911
|
-
throw new LocalError(`${
|
|
912
|
+
throw new LocalError(`${path} is a ${kind}, not a file.`);
|
|
1912
913
|
}
|
|
1913
|
-
requireAttachmentSize(
|
|
914
|
+
requireAttachmentSize(path, fileStats.size, maxBytes);
|
|
1914
915
|
try {
|
|
1915
|
-
closeSync(openSync(
|
|
916
|
+
closeSync(openSync(path, "r"));
|
|
1916
917
|
} catch (error) {
|
|
1917
|
-
throw new LocalError(`cannot read ${
|
|
918
|
+
throw new LocalError(`cannot read ${path}: ${posixDetail(error)}`);
|
|
1918
919
|
}
|
|
1919
920
|
}
|
|
1920
|
-
function requireAttachmentSize(
|
|
921
|
+
function requireAttachmentSize(path, size, maxBytes) {
|
|
1921
922
|
if (size > maxBytes) {
|
|
1922
|
-
throw new LocalError(`${
|
|
923
|
+
throw new LocalError(`${path} is too large: ${size} bytes, and attachments are capped at ${maxBytes}.`);
|
|
1923
924
|
}
|
|
1924
925
|
}
|
|
1925
926
|
function posixDetail(error) {
|
|
@@ -1928,19 +929,19 @@ function posixDetail(error) {
|
|
|
1928
929
|
if (code === "EACCES") return "permission denied";
|
|
1929
930
|
return error instanceof Error ? error.message : String(error);
|
|
1930
931
|
}
|
|
1931
|
-
async function setState(ctx, identifier,
|
|
932
|
+
async function setState(ctx, identifier, stateName) {
|
|
1932
933
|
const showBody = await ctx.client.get(`/api/v1/issues/${identifier}`);
|
|
1933
|
-
const teamKey =
|
|
934
|
+
const teamKey = str((showBody.data["team"] ?? {})["key"]);
|
|
1934
935
|
const statesBody = await ctx.client.get(
|
|
1935
936
|
`/api/v1/teams/${teamKey}/workflow_states`
|
|
1936
937
|
);
|
|
1937
|
-
const wanted =
|
|
938
|
+
const wanted = stateName.toLowerCase();
|
|
1938
939
|
const state = statesBody.data.find(
|
|
1939
|
-
(candidate) =>
|
|
940
|
+
(candidate) => str(candidate["name"]).toLowerCase() === wanted || str(candidate["type"]) === wanted
|
|
1940
941
|
);
|
|
1941
942
|
if (state === void 0) {
|
|
1942
|
-
const names = statesBody.data.map((candidate) =>
|
|
1943
|
-
throw new CommandError(`No workflow state matches "${
|
|
943
|
+
const names = statesBody.data.map((candidate) => str(candidate["name"])).join(", ");
|
|
944
|
+
throw new CommandError(`No workflow state matches "${stateName}". States: ${names}`);
|
|
1944
945
|
}
|
|
1945
946
|
const body = await ctx.client.put(`/api/v1/issues/${identifier}/state`, { state_id: state["id"] });
|
|
1946
947
|
return body.data;
|
|
@@ -1971,16 +972,16 @@ function requestedLabelInputs(values) {
|
|
|
1971
972
|
});
|
|
1972
973
|
}
|
|
1973
974
|
function normalizeLabelName(value) {
|
|
1974
|
-
return
|
|
975
|
+
return str(value).trim().toLowerCase();
|
|
1975
976
|
}
|
|
1976
977
|
async function prepareCreateLabelIds(ctx, teamKey, names) {
|
|
1977
978
|
if (names.length === 0) return [];
|
|
1978
979
|
const teams = await ctx.client.get("/api/v1/teams");
|
|
1979
980
|
const target = teams.data.find(
|
|
1980
|
-
(team) =>
|
|
981
|
+
(team) => str(team["key"]).toLowerCase() === teamKey.toLowerCase()
|
|
1981
982
|
);
|
|
1982
983
|
if (target === void 0) throw new CommandError(`No team matches key "${teamKey}".`);
|
|
1983
|
-
const teamId =
|
|
984
|
+
const teamId = str(target["id"]);
|
|
1984
985
|
if (teamId === "") throw new CommandError(`team ${teamKey} carried no id`);
|
|
1985
986
|
return prepareApplicableLabelIds(ctx, teamId, names);
|
|
1986
987
|
}
|
|
@@ -2028,7 +1029,7 @@ async function resolveOrCreateApplicableLabel(ctx, catalog, name, teamId) {
|
|
|
2028
1029
|
}
|
|
2029
1030
|
}
|
|
2030
1031
|
function labelId(label) {
|
|
2031
|
-
const id =
|
|
1032
|
+
const id = str(label["id"]);
|
|
2032
1033
|
if (id === "") throw new CommandError("the label response carried no id");
|
|
2033
1034
|
return id;
|
|
2034
1035
|
}
|
|
@@ -2057,7 +1058,7 @@ async function prepareLabelUpdate(ctx, identifier, addInputs, removeInputs) {
|
|
|
2057
1058
|
const ids = originalIds.filter((id) => !removed.has(id));
|
|
2058
1059
|
if (addInputs.length > 0) {
|
|
2059
1060
|
const team = issue["team"] ?? {};
|
|
2060
|
-
const teamId =
|
|
1061
|
+
const teamId = str(team["id"]);
|
|
2061
1062
|
if (teamId === "") throw new CommandError(`issue ${identifier} carried no team id`);
|
|
2062
1063
|
let catalog = (await ctx.client.get("/api/v1/labels")).data;
|
|
2063
1064
|
for (const input of addInputs) {
|
|
@@ -2091,15 +1092,15 @@ async function fetchIssues(ctx, params, opts) {
|
|
|
2091
1092
|
throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
|
|
2092
1093
|
}
|
|
2093
1094
|
if (opts.all === true) {
|
|
2094
|
-
const
|
|
1095
|
+
const records5 = await fetchEveryPage(
|
|
2095
1096
|
(page) => ctx.client.get("/api/v1/issues", { ...params, page })
|
|
2096
1097
|
);
|
|
2097
|
-
return { records:
|
|
1098
|
+
return { records: records5, notice: "" };
|
|
2098
1099
|
}
|
|
2099
1100
|
const body = await ctx.client.get("/api/v1/issues", params);
|
|
2100
|
-
const
|
|
1101
|
+
const records4 = body.data;
|
|
2101
1102
|
const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
|
|
2102
|
-
return { records:
|
|
1103
|
+
return { records: records4, notice: notices(body.meta, requested, records4) };
|
|
2103
1104
|
}
|
|
2104
1105
|
function listParams(ctx, opts) {
|
|
2105
1106
|
if (opts.type !== void 0 && opts.label !== void 0) {
|
|
@@ -2192,7 +1193,7 @@ function errorText(error) {
|
|
|
2192
1193
|
if (error instanceof HttpError) return `${clean(error.message)} (HTTP ${error.status})`;
|
|
2193
1194
|
return `cannot reach the API: ${clean(error.message)}`;
|
|
2194
1195
|
}
|
|
2195
|
-
function
|
|
1196
|
+
function str(value) {
|
|
2196
1197
|
return value === null || value === void 0 ? "" : String(value);
|
|
2197
1198
|
}
|
|
2198
1199
|
|
|
@@ -2201,7 +1202,7 @@ var SAFE_ID = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
|
2201
1202
|
function requireSafeId(kind, id) {
|
|
2202
1203
|
if (!SAFE_ID.test(id)) throw new UsageError(`not a ${kind} id: ${id}`);
|
|
2203
1204
|
}
|
|
2204
|
-
function
|
|
1205
|
+
function str2(value) {
|
|
2205
1206
|
return value === null || value === void 0 ? "" : String(value);
|
|
2206
1207
|
}
|
|
2207
1208
|
function field(row, ...keys) {
|
|
@@ -2220,15 +1221,15 @@ function putDate(body, key, raw) {
|
|
|
2220
1221
|
if (parsed === null) return body;
|
|
2221
1222
|
return { ...body, [key]: parsed === "clear" ? null : parsed };
|
|
2222
1223
|
}
|
|
2223
|
-
async function paginatedRead(ctx,
|
|
1224
|
+
async function paginatedRead(ctx, path, params, opts) {
|
|
2224
1225
|
if (opts.all === true && opts.page !== void 0) {
|
|
2225
1226
|
throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
|
|
2226
1227
|
}
|
|
2227
1228
|
if (opts.all === true) {
|
|
2228
|
-
const
|
|
2229
|
-
return { records:
|
|
1229
|
+
const records4 = await fetchEveryPage((page) => ctx.client.get(path, { ...params, page }));
|
|
1230
|
+
return { records: records4, notice: "" };
|
|
2230
1231
|
}
|
|
2231
|
-
const body = await ctx.client.get(
|
|
1232
|
+
const body = await ctx.client.get(path, params);
|
|
2232
1233
|
const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
|
|
2233
1234
|
return { records: body.data, notice: notices(body.meta, requested, body.data) };
|
|
2234
1235
|
}
|
|
@@ -2236,18 +1237,18 @@ async function projectList(ctx, opts) {
|
|
|
2236
1237
|
let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
|
|
2237
1238
|
if (opts.search !== void 0) params = { ...params, search: opts.search };
|
|
2238
1239
|
if (opts.state !== void 0) params = { ...params, state: opts.state };
|
|
2239
|
-
const { records:
|
|
2240
|
-
const human =
|
|
1240
|
+
const { records: records4, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
|
|
1241
|
+
const human = records4.length === 0 ? "No projects.\n" : table(
|
|
2241
1242
|
["ID", "STATE", "LEAD", "TARGET", "NAME"],
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
1243
|
+
records4.map((project) => [
|
|
1244
|
+
str2(project["id"]),
|
|
1245
|
+
str2(project["state"]) || "-",
|
|
1246
|
+
str2(field(project, "lead", "name")) || "-",
|
|
1247
|
+
str2(project["target_date"]) || "-",
|
|
1248
|
+
str2(project["name"])
|
|
2248
1249
|
])
|
|
2249
1250
|
);
|
|
2250
|
-
return { json:
|
|
1251
|
+
return { json: records4, human, notice };
|
|
2251
1252
|
}
|
|
2252
1253
|
async function projectShow(ctx, id) {
|
|
2253
1254
|
requireSafeId("project", id);
|
|
@@ -2268,12 +1269,12 @@ async function projectShow(ctx, id) {
|
|
|
2268
1269
|
];
|
|
2269
1270
|
const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
|
|
2270
1271
|
${title}:
|
|
2271
|
-
${
|
|
1272
|
+
${indent(String(text))}
|
|
2272
1273
|
`).join("");
|
|
2273
1274
|
const milestones = Array.isArray(project["milestones"]) ? project["milestones"] : [];
|
|
2274
|
-
const milestoneText = milestones.length === 0 ? "" : "\nMilestones:\n" + milestones.map((milestone) => ` ${(
|
|
1275
|
+
const milestoneText = milestones.length === 0 ? "" : "\nMilestones:\n" + milestones.map((milestone) => ` ${(str2(milestone["target_date"]) || "-").padEnd(12)}${clean(milestone["name"])}
|
|
2275
1276
|
`).join("");
|
|
2276
|
-
const human = `${
|
|
1277
|
+
const human = `${str2(project["id"])} \xB7 ${clean(project["name"])}
|
|
2277
1278
|
` + fieldLines + sectionText + milestoneText;
|
|
2278
1279
|
return { json: project, human };
|
|
2279
1280
|
}
|
|
@@ -2292,7 +1293,7 @@ async function projectCreate(ctx, opts) {
|
|
|
2292
1293
|
if (opts.name === void 0) throw new UsageError("usage: harness project create --name <name> \u2026");
|
|
2293
1294
|
const body = await ctx.client.post("/api/v1/projects", projectBody(opts));
|
|
2294
1295
|
const project = body.data;
|
|
2295
|
-
return { json: project, human: `Created project ${clean(project["name"])} (${
|
|
1296
|
+
return { json: project, human: `Created project ${clean(project["name"])} (${str2(project["id"])})
|
|
2296
1297
|
` };
|
|
2297
1298
|
}
|
|
2298
1299
|
async function projectUpdate(ctx, id, opts) {
|
|
@@ -2302,7 +1303,7 @@ async function projectUpdate(ctx, id, opts) {
|
|
|
2302
1303
|
throw new UsageError("usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026");
|
|
2303
1304
|
}
|
|
2304
1305
|
const body = await ctx.client.patch(`/api/v1/projects/${id}`, patch);
|
|
2305
|
-
return { json: body.data, human: `Updated project ${
|
|
1306
|
+
return { json: body.data, human: `Updated project ${str2(body.data["id"])}
|
|
2306
1307
|
` };
|
|
2307
1308
|
}
|
|
2308
1309
|
async function milestoneList(ctx, project) {
|
|
@@ -2312,9 +1313,9 @@ async function milestoneList(ctx, project) {
|
|
|
2312
1313
|
const human = body.data.length === 0 ? "No milestones.\n" : table(
|
|
2313
1314
|
["ID", "TARGET", "NAME"],
|
|
2314
1315
|
body.data.map((milestone) => [
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
1316
|
+
str2(milestone["id"]),
|
|
1317
|
+
str2(milestone["target_date"]) || "-",
|
|
1318
|
+
str2(milestone["name"])
|
|
2318
1319
|
])
|
|
2319
1320
|
);
|
|
2320
1321
|
return { json: body.data, human };
|
|
@@ -2331,26 +1332,26 @@ async function milestoneCreate(ctx, opts) {
|
|
|
2331
1332
|
const milestone = response.data;
|
|
2332
1333
|
return {
|
|
2333
1334
|
json: milestone,
|
|
2334
|
-
human: `Created milestone ${clean(milestone["name"])} (${
|
|
1335
|
+
human: `Created milestone ${clean(milestone["name"])} (${str2(milestone["id"])})
|
|
2335
1336
|
`
|
|
2336
1337
|
};
|
|
2337
1338
|
}
|
|
2338
1339
|
async function docList(ctx, opts) {
|
|
2339
1340
|
let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
|
|
2340
1341
|
if (opts.search !== void 0) params = { ...params, search: opts.search };
|
|
2341
|
-
const { records:
|
|
2342
|
-
const human =
|
|
1342
|
+
const { records: records4, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
|
|
1343
|
+
const human = records4.length === 0 ? "No documents.\n" : table(
|
|
2343
1344
|
["ID", "UPDATED", "TITLE"],
|
|
2344
|
-
|
|
1345
|
+
records4.map((doc) => [str2(doc["id"]), str2(doc["updated_at"]) || "-", str2(doc["title"])])
|
|
2345
1346
|
);
|
|
2346
|
-
return { json:
|
|
1347
|
+
return { json: records4, human, notice };
|
|
2347
1348
|
}
|
|
2348
1349
|
async function docShow(ctx, id) {
|
|
2349
1350
|
requireSafeId("document", id);
|
|
2350
1351
|
const body = await ctx.client.get(`/api/v1/documents/${id}`);
|
|
2351
1352
|
const doc = body.data;
|
|
2352
1353
|
const content = doc["content"];
|
|
2353
|
-
const human = `${
|
|
1354
|
+
const human = `${str2(doc["id"])} \xB7 ${clean(doc["title"])}
|
|
2354
1355
|
Updated: ${clean(doc["updated_at"])}
|
|
2355
1356
|
` + (content === null || content === void 0 || content === "" ? "" : "\n" + cleanMultiline(String(content)) + "\n");
|
|
2356
1357
|
return { json: doc, human };
|
|
@@ -2362,7 +1363,7 @@ async function docCreate(ctx, opts) {
|
|
|
2362
1363
|
body = putOpt2(body, "project_id", opts.project);
|
|
2363
1364
|
const response = await ctx.client.post("/api/v1/documents", body);
|
|
2364
1365
|
const doc = response.data;
|
|
2365
|
-
return { json: doc, human: `Created document ${clean(doc["title"])} (${
|
|
1366
|
+
return { json: doc, human: `Created document ${clean(doc["title"])} (${str2(doc["id"])})
|
|
2366
1367
|
` };
|
|
2367
1368
|
}
|
|
2368
1369
|
async function docUpdate(ctx, id, opts) {
|
|
@@ -2375,7 +1376,7 @@ async function docUpdate(ctx, id, opts) {
|
|
|
2375
1376
|
throw new UsageError("usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026");
|
|
2376
1377
|
}
|
|
2377
1378
|
const response = await ctx.client.patch(`/api/v1/documents/${id}`, patch);
|
|
2378
|
-
return { json: response.data, human: `Updated document ${
|
|
1379
|
+
return { json: response.data, human: `Updated document ${str2(response.data["id"])}
|
|
2379
1380
|
` };
|
|
2380
1381
|
}
|
|
2381
1382
|
async function linkList(ctx, issue) {
|
|
@@ -2383,7 +1384,7 @@ async function linkList(ctx, issue) {
|
|
|
2383
1384
|
const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
|
|
2384
1385
|
const human = body.data.length === 0 ? "No links.\n" : table(
|
|
2385
1386
|
["ID", "URL", "TITLE"],
|
|
2386
|
-
body.data.map((link) => [
|
|
1387
|
+
body.data.map((link) => [str2(link["id"]), str2(link["url"]), str2(link["title"]) || "-"])
|
|
2387
1388
|
);
|
|
2388
1389
|
return { json: body.data, human };
|
|
2389
1390
|
}
|
|
@@ -2416,7 +1417,7 @@ async function linkUpdate(ctx, issue, idOrUrl, opts) {
|
|
|
2416
1417
|
if (Object.keys(patch).length === 0) {
|
|
2417
1418
|
throw new UsageError("usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>");
|
|
2418
1419
|
}
|
|
2419
|
-
const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() :
|
|
1420
|
+
const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() : str2((await findLink(ctx, issue, idOrUrl))["id"]);
|
|
2420
1421
|
if (!isUuid(idOrUrl)) patch["expected_url"] = idOrUrl;
|
|
2421
1422
|
const response = await ctx.client.patch(`/api/v1/issues/${issue}/links/${id}`, patch);
|
|
2422
1423
|
return { json: response.data, human: `Updated link ${clean(response.data["url"])} on ${issue}
|
|
@@ -2425,19 +1426,19 @@ async function linkUpdate(ctx, issue, idOrUrl, opts) {
|
|
|
2425
1426
|
async function linkRm(ctx, issue, idOrUrl) {
|
|
2426
1427
|
requireIdentifiers([issue]);
|
|
2427
1428
|
const link = await findLink(ctx, issue, idOrUrl, true);
|
|
2428
|
-
await ctx.client.delete(`/api/v1/issues/${issue}/links/${
|
|
1429
|
+
await ctx.client.delete(`/api/v1/issues/${issue}/links/${str2(link["id"])}`);
|
|
2429
1430
|
return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
|
|
2430
1431
|
` };
|
|
2431
1432
|
}
|
|
2432
|
-
async function
|
|
1433
|
+
async function labelList(ctx) {
|
|
2433
1434
|
const body = await ctx.client.get("/api/v1/labels");
|
|
2434
1435
|
const human = body.data.length === 0 ? "No labels.\n" : table(
|
|
2435
1436
|
["NAME", "COLOR", "ISSUES", "ID"],
|
|
2436
1437
|
body.data.map((label) => [
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
1438
|
+
str2(label["name"]),
|
|
1439
|
+
str2(label["color"]) || "-",
|
|
1440
|
+
str2(label["issue_count"] ?? 0),
|
|
1441
|
+
str2(label["id"])
|
|
2441
1442
|
])
|
|
2442
1443
|
);
|
|
2443
1444
|
return { json: body.data, human };
|
|
@@ -2460,11 +1461,11 @@ async function teamCreate(ctx, opts) {
|
|
|
2460
1461
|
}
|
|
2461
1462
|
const created = await ctx.client.post("/api/v1/teams", { key: opts.key, name: opts.name });
|
|
2462
1463
|
const team = created.data;
|
|
2463
|
-
const states = await ctx.client.get(`/api/v1/teams/${
|
|
2464
|
-
const human = `Created team ${
|
|
1464
|
+
const states = await ctx.client.get(`/api/v1/teams/${str2(team["key"])}/workflow_states`);
|
|
1465
|
+
const human = `Created team ${str2(team["key"])} \u2014 ${clean(team["name"])}
|
|
2465
1466
|
` + table(
|
|
2466
1467
|
["NAME", "TYPE"],
|
|
2467
|
-
states.data.map((state) => [
|
|
1468
|
+
states.data.map((state) => [str2(state["name"]), str2(state["type"])])
|
|
2468
1469
|
);
|
|
2469
1470
|
return { json: { ...team, workflow_states: states.data }, human };
|
|
2470
1471
|
}
|
|
@@ -2473,9 +1474,9 @@ async function teamList(ctx) {
|
|
|
2473
1474
|
const human = body.data.length === 0 ? "No teams.\n" : table(
|
|
2474
1475
|
["KEY", "NAME", "ISSUES", "DEFAULT"],
|
|
2475
1476
|
body.data.map((team) => [
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
1477
|
+
str2(team["key"]),
|
|
1478
|
+
str2(team["name"]),
|
|
1479
|
+
str2(team["issue_count"] ?? 0),
|
|
2479
1480
|
team["is_default"] === true ? "yes" : ""
|
|
2480
1481
|
])
|
|
2481
1482
|
);
|
|
@@ -2487,10 +1488,10 @@ async function workflowStates(ctx, team) {
|
|
|
2487
1488
|
const human = body.data.length === 0 ? "No workflow states.\n" : table(
|
|
2488
1489
|
["NAME", "TYPE", "COLOR", "ID"],
|
|
2489
1490
|
body.data.map((state) => [
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
1491
|
+
str2(state["name"]),
|
|
1492
|
+
str2(state["type"]),
|
|
1493
|
+
str2(state["color"]) || "-",
|
|
1494
|
+
str2(state["id"])
|
|
2494
1495
|
])
|
|
2495
1496
|
);
|
|
2496
1497
|
return { json: body.data, human };
|
|
@@ -2499,7 +1500,7 @@ async function userList(ctx) {
|
|
|
2499
1500
|
const body = await ctx.client.get("/api/v1/users");
|
|
2500
1501
|
const human = body.data.length === 0 ? "No users.\n" : table(
|
|
2501
1502
|
["NAME", "EMAIL", "ID"],
|
|
2502
|
-
body.data.map((user) => [
|
|
1503
|
+
body.data.map((user) => [str2(user["name"]), str2(user["email"]), str2(user["id"])])
|
|
2503
1504
|
);
|
|
2504
1505
|
return { json: body.data, human };
|
|
2505
1506
|
}
|
|
@@ -2508,7 +1509,7 @@ async function teamRename(ctx, key, name) {
|
|
|
2508
1509
|
throw new UsageError("usage: harness team rename <key> --name <name>");
|
|
2509
1510
|
}
|
|
2510
1511
|
const body = await ctx.client.patch(`/api/v1/teams/${encodeURIComponent(key)}`, { name });
|
|
2511
|
-
return { json: body.data, human: `Renamed ${
|
|
1512
|
+
return { json: body.data, human: `Renamed ${str2(body.data["key"])} to ${clean(body.data["name"])}
|
|
2512
1513
|
` };
|
|
2513
1514
|
}
|
|
2514
1515
|
async function teamSetDefault(ctx, key) {
|
|
@@ -2516,7 +1517,7 @@ async function teamSetDefault(ctx, key) {
|
|
|
2516
1517
|
const body = await ctx.client.put(`/api/v1/teams/${encodeURIComponent(key)}/default`, {});
|
|
2517
1518
|
return {
|
|
2518
1519
|
json: body.data,
|
|
2519
|
-
human: `${
|
|
1520
|
+
human: `${str2(body.data["key"])} is now the default filing target
|
|
2520
1521
|
`
|
|
2521
1522
|
};
|
|
2522
1523
|
}
|
|
@@ -2555,7 +1556,7 @@ async function stateOrder(ctx, ids, opts) {
|
|
|
2555
1556
|
const body = await ctx.client.put(`/api/v1/teams/${key}/workflow_states/order`, { state_ids });
|
|
2556
1557
|
const human = table(
|
|
2557
1558
|
["NAME", "TYPE"],
|
|
2558
|
-
body.data.map((state) => [
|
|
1559
|
+
body.data.map((state) => [str2(state["name"]), str2(state["type"])])
|
|
2559
1560
|
);
|
|
2560
1561
|
return { json: body.data, human };
|
|
2561
1562
|
}
|
|
@@ -2591,11 +1592,11 @@ async function memberList(ctx) {
|
|
|
2591
1592
|
const human = body.data.length === 0 ? "No members.\n" : table(
|
|
2592
1593
|
["NAME", "EMAIL", "RANK", "STANDING", "ID"],
|
|
2593
1594
|
body.data.map((member) => [
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
1595
|
+
str2(field(member, "user", "name")),
|
|
1596
|
+
str2(field(member, "user", "email")),
|
|
1597
|
+
str2(member["role"]) || "-",
|
|
1598
|
+
str2(member["standing"]),
|
|
1599
|
+
str2(field(member, "user", "id"))
|
|
2599
1600
|
])
|
|
2600
1601
|
);
|
|
2601
1602
|
return { json: body.data, human };
|
|
@@ -2607,7 +1608,7 @@ async function memberAdd(ctx, opts) {
|
|
|
2607
1608
|
const body = await ctx.client.post("/api/v1/members", { email: opts.email, role: opts.role });
|
|
2608
1609
|
return {
|
|
2609
1610
|
json: body.data,
|
|
2610
|
-
human: `Added ${
|
|
1611
|
+
human: `Added ${str2(field(body.data, "user", "email"))} as ${str2(body.data["role"])}
|
|
2611
1612
|
`
|
|
2612
1613
|
};
|
|
2613
1614
|
}
|
|
@@ -2616,19 +1617,19 @@ async function memberSetRole(ctx, userId, opts) {
|
|
|
2616
1617
|
throw new UsageError("usage: harness member set-role <user-id> --role <admin|staff|collaborator|none>");
|
|
2617
1618
|
}
|
|
2618
1619
|
const body = await ctx.client.put(`/api/v1/members/${encodeURIComponent(userId)}/role`, { role: opts.role });
|
|
2619
|
-
const rank =
|
|
2620
|
-
const human = rank === "" ? `Revoked the rank for ${
|
|
2621
|
-
` : `${
|
|
1620
|
+
const rank = str2(body.data["role"]);
|
|
1621
|
+
const human = rank === "" ? `Revoked the rank for ${str2(field(body.data, "user", "email"))}
|
|
1622
|
+
` : `${str2(field(body.data, "user", "email"))} is now ${rank}
|
|
2622
1623
|
`;
|
|
2623
1624
|
return { json: body.data, human };
|
|
2624
1625
|
}
|
|
2625
1626
|
function requireYes(command, id, yes) {
|
|
2626
1627
|
if (yes !== true) throw new UsageError(`${command} ${id} is destructive \u2014 pass --yes to confirm`);
|
|
2627
1628
|
}
|
|
2628
|
-
async function upload(ctx,
|
|
2629
|
-
return attachFile(ctx,
|
|
1629
|
+
async function upload(ctx, path) {
|
|
1630
|
+
return attachFile(ctx, path);
|
|
2630
1631
|
}
|
|
2631
|
-
function
|
|
1632
|
+
function indent(text) {
|
|
2632
1633
|
return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
|
|
2633
1634
|
}
|
|
2634
1635
|
|
|
@@ -2669,12 +1670,12 @@ async function toolsSearch(ctx, query, opts) {
|
|
|
2669
1670
|
}
|
|
2670
1671
|
async function toolsDomains(ctx) {
|
|
2671
1672
|
const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/capabilities/domains"));
|
|
2672
|
-
const domains =
|
|
1673
|
+
const domains = records(body.data, "domains");
|
|
2673
1674
|
return {
|
|
2674
1675
|
json: domains,
|
|
2675
1676
|
human: domains.length === 0 ? "Operator can use no capabilities here.\n" : table(
|
|
2676
1677
|
["DOMAIN", "CAPABILITIES"],
|
|
2677
|
-
domains.map((domain) => [
|
|
1678
|
+
domains.map((domain) => [str3(domain["domain"]), str3(domain["count"])])
|
|
2678
1679
|
)
|
|
2679
1680
|
};
|
|
2680
1681
|
}
|
|
@@ -2739,7 +1740,7 @@ async function toolShow(ctx, id, opts) {
|
|
|
2739
1740
|
}
|
|
2740
1741
|
async function skillsList(ctx) {
|
|
2741
1742
|
const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/skills"));
|
|
2742
|
-
const skills =
|
|
1743
|
+
const skills = records(body.data, "skills").map((skill) => ({
|
|
2743
1744
|
id: skill["id"],
|
|
2744
1745
|
title: skill["title"],
|
|
2745
1746
|
invocation_name: skill["invocation_name"],
|
|
@@ -2750,10 +1751,10 @@ async function skillsList(ctx) {
|
|
|
2750
1751
|
human: skills.length === 0 ? "No skills are assigned to Operator in this organization.\n" : table(
|
|
2751
1752
|
["INVOCATION", "TITLE", "SHA-256", "ID"],
|
|
2752
1753
|
skills.map((skill) => [
|
|
2753
|
-
|
|
1754
|
+
str3(skill.invocation_name),
|
|
2754
1755
|
truncate(skill.title),
|
|
2755
|
-
|
|
2756
|
-
|
|
1756
|
+
str3(skill.content_sha256),
|
|
1757
|
+
str3(skill.id)
|
|
2757
1758
|
])
|
|
2758
1759
|
)
|
|
2759
1760
|
};
|
|
@@ -2813,7 +1814,7 @@ async function capabilityPage(ctx, params, staleCursor) {
|
|
|
2813
1814
|
void 0,
|
|
2814
1815
|
staleCursor
|
|
2815
1816
|
);
|
|
2816
|
-
const items =
|
|
1817
|
+
const items = records(body.data, "capabilities");
|
|
2817
1818
|
const meta = body.meta;
|
|
2818
1819
|
if (!isRecord(meta)) throw new CommandError("The capabilities response has no pagination metadata.");
|
|
2819
1820
|
const nextCursor = meta.next_cursor;
|
|
@@ -2848,14 +1849,14 @@ function explain(error, notFound2, staleCursor) {
|
|
|
2848
1849
|
function capabilitiesTable(items, search2) {
|
|
2849
1850
|
const headers = ["ID", "KIND", "DOMAIN", "SUMMARY"];
|
|
2850
1851
|
const rows = items.map((item) => [
|
|
2851
|
-
|
|
1852
|
+
str3(item["id"]),
|
|
2852
1853
|
kindName(item),
|
|
2853
|
-
|
|
1854
|
+
str3(item["domain"]),
|
|
2854
1855
|
truncate(item["summary"])
|
|
2855
1856
|
]);
|
|
2856
1857
|
const table2 = search2 ? table(
|
|
2857
1858
|
["SCORE", ...headers],
|
|
2858
|
-
rows.map((row, index) => [
|
|
1859
|
+
rows.map((row, index) => [str3(items[index]["score"]), ...row])
|
|
2859
1860
|
) : table(headers, rows);
|
|
2860
1861
|
return table2 + (items.some(needsApproval) ? "\n" + APPROVAL_NOTE : "");
|
|
2861
1862
|
}
|
|
@@ -2869,14 +1870,14 @@ function capabilityDetail(capability, view, schema) {
|
|
|
2869
1870
|
const kind = kindName(capability);
|
|
2870
1871
|
const line = (label, value) => ` ${(label + ":").padEnd(10)}${clean(value)}
|
|
2871
1872
|
`;
|
|
2872
|
-
const description = cleanMultiline(
|
|
1873
|
+
const description = cleanMultiline(str3(capability["description"]));
|
|
2873
1874
|
return `${clean(capability["id"])}
|
|
2874
|
-
` + line("Domain",
|
|
1875
|
+
` + line("Domain", str3(capability["domain"])) + line("Kind", kind === "send" ? "send \u2014 delivered beyond the platform" : kind) + line("Mode", `${str3(capability["mode"])} (effect: ${str3(capability["effect"])})`) + line("Summary", str3(capability["summary"])) + line("Approval", approval(capability)) + (description === "" ? "" : `
|
|
2875
1876
|
Description:
|
|
2876
|
-
${
|
|
1877
|
+
${indent2(description)}
|
|
2877
1878
|
`) + `
|
|
2878
1879
|
${view === "input" ? "Input" : "Result"} schema:
|
|
2879
|
-
${
|
|
1880
|
+
${indent2(cleanMultiline(JSON.stringify(schema, null, 2)))}
|
|
2880
1881
|
`;
|
|
2881
1882
|
}
|
|
2882
1883
|
function needsApproval(capability) {
|
|
@@ -2896,7 +1897,7 @@ var SCHEMA_PAGING_FIELDS = /* @__PURE__ */ new Set(["schema", "schema_encoding",
|
|
|
2896
1897
|
function withoutSchemaPaging(capability) {
|
|
2897
1898
|
return Object.fromEntries(Object.entries(capability).filter(([key]) => !SCHEMA_PAGING_FIELDS.has(key)));
|
|
2898
1899
|
}
|
|
2899
|
-
function
|
|
1900
|
+
function records(value, what) {
|
|
2900
1901
|
if (!Array.isArray(value) || !value.every(isRecord)) {
|
|
2901
1902
|
throw new CommandError(`The ${what} response carried no list.`);
|
|
2902
1903
|
}
|
|
@@ -2910,13 +1911,13 @@ function dedupeById(items) {
|
|
|
2910
1911
|
return true;
|
|
2911
1912
|
});
|
|
2912
1913
|
}
|
|
2913
|
-
function
|
|
1914
|
+
function indent2(text) {
|
|
2914
1915
|
return text.split("\n").map((line) => " " + line).join("\n");
|
|
2915
1916
|
}
|
|
2916
1917
|
function isRecord(value) {
|
|
2917
1918
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2918
1919
|
}
|
|
2919
|
-
function
|
|
1920
|
+
function str3(value) {
|
|
2920
1921
|
return value === null || value === void 0 ? "" : String(value);
|
|
2921
1922
|
}
|
|
2922
1923
|
|
|
@@ -3113,11 +2114,11 @@ async function jobChangeSet(ctx, id) {
|
|
|
3113
2114
|
if (!isRecord2(body.data)) throw new CommandError("The change set response carried no change set.");
|
|
3114
2115
|
return {
|
|
3115
2116
|
...pick(body.data, CHANGE_SET_FIELDS),
|
|
3116
|
-
actions:
|
|
2117
|
+
actions: records2(body.data["actions"]).map((action) => pick(action, CHANGE_SET_ACTION_FIELDS))
|
|
3117
2118
|
};
|
|
3118
2119
|
}
|
|
3119
2120
|
function jobPeople(job) {
|
|
3120
|
-
const participants =
|
|
2121
|
+
const participants = records2(field2(job, "people", "participants"));
|
|
3121
2122
|
const owner = isRecord2(job["owner"]) ? job["owner"] : null;
|
|
3122
2123
|
const names = /* @__PURE__ */ new Map();
|
|
3123
2124
|
for (const named2 of owner === null ? participants : [...participants, owner]) {
|
|
@@ -3188,7 +2189,7 @@ async function jobWatch(ctx, id, opts) {
|
|
|
3188
2189
|
after = page.nextAfter;
|
|
3189
2190
|
job = page.job;
|
|
3190
2191
|
if (page.hasMore) continue;
|
|
3191
|
-
const state =
|
|
2192
|
+
const state = str4(job["state"]);
|
|
3192
2193
|
const waitingOn = typeof job["waiting_on"] === "string" ? job["waiting_on"] : null;
|
|
3193
2194
|
if (shown === null || shown.state !== state || shown.waitingOn !== waitingOn) {
|
|
3194
2195
|
emit(stateLine(shown?.state ?? null, state, waitingOn));
|
|
@@ -3329,13 +2330,13 @@ async function explained(send, why) {
|
|
|
3329
2330
|
function jobsTable(jobs) {
|
|
3330
2331
|
if (jobs.length === 0) return "No Operator jobs.\n";
|
|
3331
2332
|
const rows = jobs.map((job) => [
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
2333
|
+
str4(job["id"]),
|
|
2334
|
+
str4(job["state"]) || "-",
|
|
2335
|
+
str4(job["waiting_on"]) || "-",
|
|
2336
|
+
str4(job["objective_kind"]) || "-",
|
|
3336
2337
|
personName(job["owner"]),
|
|
3337
|
-
|
|
3338
|
-
|
|
2338
|
+
str4(field2(job, "plan_step", "issue")) || "-",
|
|
2339
|
+
str4(job["updated_at"]) || "-"
|
|
3339
2340
|
]);
|
|
3340
2341
|
const table2 = table(["ID", "STATE", "WAITING ON", "KIND", "OWNER", "PLAN STEP", "UPDATED"], rows);
|
|
3341
2342
|
const approvals = jobs.filter((job) => typeof job["approval_url"] === "string");
|
|
@@ -3344,30 +2345,30 @@ function jobsTable(jobs) {
|
|
|
3344
2345
|
`).join("");
|
|
3345
2346
|
}
|
|
3346
2347
|
function jobDetail(job) {
|
|
3347
|
-
const waitingOn =
|
|
2348
|
+
const waitingOn = str4(job["waiting_on"]);
|
|
3348
2349
|
const header = `Operator job ${clean(job["id"])} \xB7 ${clean(job["state"])}` + (waitingOn === "" ? "" : ` \xB7 waiting on ${clean(waitingOn)}`) + "\n";
|
|
3349
|
-
const people =
|
|
2350
|
+
const people = records2(field2(job, "people", "participants"));
|
|
3350
2351
|
const fields = [
|
|
3351
|
-
["Kind",
|
|
3352
|
-
["Role",
|
|
2352
|
+
["Kind", str4(job["objective_kind"]) || "-"],
|
|
2353
|
+
["Role", str4(job["viewer_role"]) || null],
|
|
3353
2354
|
["Owner", isRecord2(job["owner"]) ? personName(job["owner"]) : null],
|
|
3354
2355
|
["Plan step", planStepLine(job["plan_step"])],
|
|
3355
2356
|
["Plan", planLine(job["plan"])],
|
|
3356
2357
|
["Progress", progressLine(job["progress"])],
|
|
3357
2358
|
["Window", windowLine(job["window"])],
|
|
3358
2359
|
["Spend", spendLine(job["spend"])],
|
|
3359
|
-
["Verification",
|
|
2360
|
+
["Verification", str4(field2(job, "verification", "status")) || null],
|
|
3360
2361
|
["Effects", effectsLine(job["effects"])],
|
|
3361
2362
|
["Skill", skillLine(job["skill"])],
|
|
3362
2363
|
["Grant", grantLine(job["grant"], people)],
|
|
3363
|
-
["Created",
|
|
3364
|
-
["Updated",
|
|
3365
|
-
["Workbench",
|
|
2364
|
+
["Created", str4(job["inserted_at"]) || null],
|
|
2365
|
+
["Updated", str4(job["updated_at"]) || null],
|
|
2366
|
+
["Workbench", str4(job["url"]) || null]
|
|
3366
2367
|
];
|
|
3367
|
-
return header + labelled(fields) + approvalSection(job["pending_approval"], people) + listSection("Blockers",
|
|
2368
|
+
return header + labelled(fields) + approvalSection(job["pending_approval"], people) + listSection("Blockers", records2(job["blockers"]), (item) => `${str4(item["kind"])}: ${str4(item["state"])}`) + listSection("Artifacts", records2(job["artifacts"]), (item) => `${str4(item["id"])} ${str4(item["url"])}`) + peopleSection(people) + listSection(
|
|
3368
2369
|
"Procedures",
|
|
3369
|
-
|
|
3370
|
-
(procedure) => `${
|
|
2370
|
+
records2(job["procedures"]),
|
|
2371
|
+
(procedure) => `${str4(procedure["name"])} v${str4(procedure["current_version"])} ${str4(procedure["id"])}`
|
|
3371
2372
|
) + 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";
|
|
3372
2373
|
}
|
|
3373
2374
|
function labelled(fields, width = 15) {
|
|
@@ -3376,16 +2377,16 @@ function labelled(fields, width = 15) {
|
|
|
3376
2377
|
}
|
|
3377
2378
|
function planStepLine(step) {
|
|
3378
2379
|
if (!isRecord2(step)) return null;
|
|
3379
|
-
return `${
|
|
2380
|
+
return `${str4(step["issue"])} (plan ${str4(step["plan_id"])})`;
|
|
3380
2381
|
}
|
|
3381
2382
|
function planLine(plan) {
|
|
3382
2383
|
if (!isRecord2(plan)) return "none yet";
|
|
3383
2384
|
const count = plan["capability_count"];
|
|
3384
|
-
return `version ${
|
|
2385
|
+
return `version ${str4(plan["version"])} \xB7 instructions v${str4(plan["instruction_version"])} \xB7 ${str4(count)} capabilit${count === 1 ? "y" : "ies"}`;
|
|
3385
2386
|
}
|
|
3386
2387
|
function progressLine(progress) {
|
|
3387
2388
|
if (!isRecord2(progress)) return null;
|
|
3388
|
-
const parts = [
|
|
2389
|
+
const parts = [str4(progress["stage"]), str4(progress["situation"])].filter((part) => part !== "");
|
|
3389
2390
|
if (progress["needs_viewer"] === true) parts.push("needs you");
|
|
3390
2391
|
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3391
2392
|
}
|
|
@@ -3394,28 +2395,28 @@ function windowLine(window) {
|
|
|
3394
2395
|
const left = duration(window["remaining_ms"]);
|
|
3395
2396
|
const budget = duration(window["budget_ms"]);
|
|
3396
2397
|
const sequence = window["sequence"];
|
|
3397
|
-
return `${left ?? "?"} left${budget === null ? "" : ` of ${budget}`} \xB7 ${
|
|
2398
|
+
return `${left ?? "?"} left${budget === null ? "" : ` of ${budget}`} \xB7 ${str4(window["state"])}` + (typeof sequence === "number" ? ` (window ${sequence})` : "");
|
|
3398
2399
|
}
|
|
3399
2400
|
function spendLine(spend) {
|
|
3400
2401
|
if (!isRecord2(spend)) return null;
|
|
3401
|
-
const spent = `${
|
|
2402
|
+
const spent = `${str4(spend["spent_cents"]) || "0"} cents spent`;
|
|
3402
2403
|
if (typeof spend["plan_budget_cents"] !== "number") return spent;
|
|
3403
2404
|
const remaining = spend["plan_remaining_cents"];
|
|
3404
2405
|
return `${spent} \xB7 plan budget ${spend["plan_budget_cents"]} cents` + (typeof remaining === "number" ? `, ${remaining} cents left` : "");
|
|
3405
2406
|
}
|
|
3406
2407
|
function effectsLine(effects) {
|
|
3407
2408
|
if (!isRecord2(effects)) return null;
|
|
3408
|
-
return ["staged", "succeeded", "failed", "unknown"].map((key) => `${
|
|
2409
|
+
return ["staged", "succeeded", "failed", "unknown"].map((key) => `${str4(effects[key]) || "0"} ${key}`).join(" \xB7 ");
|
|
3409
2410
|
}
|
|
3410
2411
|
function skillLine(skill) {
|
|
3411
2412
|
if (!isRecord2(skill)) return null;
|
|
3412
|
-
return `${
|
|
2413
|
+
return `${str4(skill["title"])} (${str4(skill["invocation_name"])}) \xB7 sha256 ${str4(skill["content_sha256"])} \xB7 pinned ${str4(skill["pinned_at"])}`;
|
|
3413
2414
|
}
|
|
3414
2415
|
function grantLine(grant, people) {
|
|
3415
2416
|
if (!isRecord2(grant)) return null;
|
|
3416
|
-
const approvedAt =
|
|
3417
|
-
const expiresAt =
|
|
3418
|
-
return `${
|
|
2417
|
+
const approvedAt = str4(grant["approved_at"]);
|
|
2418
|
+
const expiresAt = str4(grant["expires_at"]);
|
|
2419
|
+
return `${str4(grant["status"])} v${str4(grant["version"])} \xB7 approved by ${nameOf(grant["approver_user_id"], people)}` + (approvedAt === "" ? "" : ` at ${approvedAt}`) + ` \xB7 ${expiresAt === "" ? "no expiry" : `expires ${expiresAt}`}`;
|
|
3419
2420
|
}
|
|
3420
2421
|
function approvalSection(pending, people) {
|
|
3421
2422
|
if (!isRecord2(pending)) return "";
|
|
@@ -3424,19 +2425,19 @@ function approvalSection(pending, people) {
|
|
|
3424
2425
|
return "\nWaiting for approval. Approve it in the app; the CLI cannot:\n" + labelled([
|
|
3425
2426
|
[
|
|
3426
2427
|
"Change set",
|
|
3427
|
-
`${
|
|
2428
|
+
`${str4(pending["manifest_id"])} v${str4(pending["version"])} \xB7 ${str4(pending["action_count"])} actions \xB7 up to ${str4(pending["max_effect_count"])} effects \xB7 plan version ${str4(pending["plan_version"])}`
|
|
3428
2429
|
],
|
|
3429
|
-
["Digest",
|
|
2430
|
+
["Digest", str4(pending["digest"]) || null],
|
|
3430
2431
|
["Approvers", approvers === "" ? canApprove : `${approvers} \xB7 ${canApprove}`],
|
|
3431
|
-
["Approve at",
|
|
2432
|
+
["Approve at", str4(pending["url"]) || null]
|
|
3432
2433
|
]);
|
|
3433
2434
|
}
|
|
3434
2435
|
function peopleSection(people) {
|
|
3435
2436
|
if (people.length === 0) return "";
|
|
3436
2437
|
const rows = people.map((person) => [
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
2438
|
+
str4(person["name"]) || "(no name)",
|
|
2439
|
+
str4(person["role"]),
|
|
2440
|
+
str4(person["user_id"])
|
|
3440
2441
|
]);
|
|
3441
2442
|
const lines = table(["", "", ""], rows).split("\n").slice(1, -1);
|
|
3442
2443
|
return "\nPeople:\n" + lines.map((line) => ` ${line}
|
|
@@ -3445,15 +2446,15 @@ function peopleSection(people) {
|
|
|
3445
2446
|
function changesText(id, job, changeSet) {
|
|
3446
2447
|
if (changeSet === null) return `No pending change set: Operator has proposed nothing for job ${id} yet.
|
|
3447
2448
|
`;
|
|
3448
|
-
const approvalUrl =
|
|
3449
|
-
const rows =
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
(Array.isArray(action["required_capability_ids"]) ? action["required_capability_ids"].map(
|
|
3455
|
-
|
|
3456
|
-
|
|
2449
|
+
const approvalUrl = str4(job["approval_url"]);
|
|
2450
|
+
const rows = records2(changeSet["actions"]).map((action) => [
|
|
2451
|
+
str4(action["display_order"]) || "-",
|
|
2452
|
+
str4(action["action_type"]),
|
|
2453
|
+
str4(action["action_version"]),
|
|
2454
|
+
str4(action["effect_count"]),
|
|
2455
|
+
(Array.isArray(action["required_capability_ids"]) ? action["required_capability_ids"].map(str4).join(", ") : "") || "-",
|
|
2456
|
+
str4(action["digest"]),
|
|
2457
|
+
str4(action["id"])
|
|
3457
2458
|
]);
|
|
3458
2459
|
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}
|
|
3459
2460
|
`).join("");
|
|
@@ -3461,14 +2462,14 @@ function changesText(id, job, changeSet) {
|
|
|
3461
2462
|
` + labelled([
|
|
3462
2463
|
[
|
|
3463
2464
|
"Change set",
|
|
3464
|
-
`${
|
|
2465
|
+
`${str4(changeSet["manifest_id"])} v${str4(changeSet["version"])} \xB7 plan version ${str4(changeSet["plan_version"])} \xB7 up to ${str4(changeSet["max_effect_count"])} effects`
|
|
3465
2466
|
],
|
|
3466
|
-
["Digest",
|
|
3467
|
-
approvalUrl === "" ? ["Workbench",
|
|
2467
|
+
["Digest", str4(changeSet["digest"]) || null],
|
|
2468
|
+
approvalUrl === "" ? ["Workbench", str4(job["url"]) || null] : ["Approve at", approvalUrl]
|
|
3468
2469
|
]) + 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");
|
|
3469
2470
|
}
|
|
3470
2471
|
function peopleText(id, people) {
|
|
3471
|
-
const one = (entry) => isRecord2(entry) ?
|
|
2472
|
+
const one = (entry) => isRecord2(entry) ? str4(entry["name"]) || str4(entry["user_id"]) : "-";
|
|
3472
2473
|
const many = (entries) => (Array.isArray(entries) ? entries.map(one).join(", ") : "") || "none";
|
|
3473
2474
|
return `People on Operator job ${id}:
|
|
3474
2475
|
` + labelled(
|
|
@@ -3576,7 +2577,7 @@ function plural(count, noun) {
|
|
|
3576
2577
|
function uniqueById(jobs) {
|
|
3577
2578
|
const seen = /* @__PURE__ */ new Set();
|
|
3578
2579
|
return jobs.filter((job) => {
|
|
3579
|
-
const id =
|
|
2580
|
+
const id = str4(job["id"]);
|
|
3580
2581
|
if (seen.has(id)) return false;
|
|
3581
2582
|
seen.add(id);
|
|
3582
2583
|
return true;
|
|
@@ -3584,12 +2585,12 @@ function uniqueById(jobs) {
|
|
|
3584
2585
|
}
|
|
3585
2586
|
function personName(person) {
|
|
3586
2587
|
if (!isRecord2(person)) return "-";
|
|
3587
|
-
return
|
|
2588
|
+
return str4(person["name"]) || str4(person["user_id"]) || "-";
|
|
3588
2589
|
}
|
|
3589
2590
|
function nameOf(userId, people) {
|
|
3590
|
-
const id =
|
|
2591
|
+
const id = str4(userId);
|
|
3591
2592
|
const person = people.find((candidate) => candidate["user_id"] === id);
|
|
3592
|
-
return (person === void 0 ? "" :
|
|
2593
|
+
return (person === void 0 ? "" : str4(person["name"])) || id || "?";
|
|
3593
2594
|
}
|
|
3594
2595
|
function duration(ms) {
|
|
3595
2596
|
if (typeof ms !== "number" || !Number.isFinite(ms)) return null;
|
|
@@ -3615,13 +2616,13 @@ function field2(record, ...keys) {
|
|
|
3615
2616
|
}
|
|
3616
2617
|
return current ?? null;
|
|
3617
2618
|
}
|
|
3618
|
-
function
|
|
2619
|
+
function records2(value) {
|
|
3619
2620
|
return Array.isArray(value) ? value.filter(isRecord2) : [];
|
|
3620
2621
|
}
|
|
3621
2622
|
function isRecord2(value) {
|
|
3622
2623
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3623
2624
|
}
|
|
3624
|
-
function
|
|
2625
|
+
function str4(value) {
|
|
3625
2626
|
return value === null || value === void 0 ? "" : String(value);
|
|
3626
2627
|
}
|
|
3627
2628
|
|
|
@@ -3666,9 +2667,9 @@ function outcomeUnknown(why, write2) {
|
|
|
3666
2667
|
`Outcome unknown: ${why}, so ${write2.mayHave}. It is not retried; check ${write2.check} before trying again.`
|
|
3667
2668
|
);
|
|
3668
2669
|
}
|
|
3669
|
-
async function sendOnce(ctx,
|
|
2670
|
+
async function sendOnce(ctx, path, body, write2) {
|
|
3670
2671
|
try {
|
|
3671
|
-
return await ctx.client.post(
|
|
2672
|
+
return await ctx.client.post(path, body, { actor: false });
|
|
3672
2673
|
} catch (error) {
|
|
3673
2674
|
if (error instanceof TransportError) {
|
|
3674
2675
|
if (neverSent(error)) {
|
|
@@ -3807,14 +2808,14 @@ function startedText(job) {
|
|
|
3807
2808
|
async function run(ctx, request, opts = {}) {
|
|
3808
2809
|
const job = await startRun(ctx, request);
|
|
3809
2810
|
if (opts.watch !== true) return { json: job, human: startedText(job) };
|
|
3810
|
-
const
|
|
3811
|
-
if (!
|
|
2811
|
+
const json = opts.json === true;
|
|
2812
|
+
if (!json) opts.onLine?.(startedText(job) + "\n");
|
|
3812
2813
|
let watched;
|
|
3813
2814
|
try {
|
|
3814
2815
|
watched = await jobWatch(ctx, job.job_id, {
|
|
3815
2816
|
throughWaits: true,
|
|
3816
|
-
collectEvents:
|
|
3817
|
-
onLine:
|
|
2817
|
+
collectEvents: json,
|
|
2818
|
+
onLine: json ? void 0 : opts.onLine,
|
|
3818
2819
|
signal: opts.signal,
|
|
3819
2820
|
sleep: opts.sleep
|
|
3820
2821
|
});
|
|
@@ -3906,18 +2907,18 @@ function resolveTextSources(sources) {
|
|
|
3906
2907
|
}
|
|
3907
2908
|
return resolved;
|
|
3908
2909
|
}
|
|
3909
|
-
function readFileOption(flag,
|
|
3910
|
-
if (
|
|
2910
|
+
function readFileOption(flag, path) {
|
|
2911
|
+
if (path === STDIN_PATH) {
|
|
3911
2912
|
if (isatty(0)) {
|
|
3912
2913
|
throw new UsageError(`${flag} - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`);
|
|
3913
2914
|
}
|
|
3914
2915
|
} else {
|
|
3915
|
-
checkReadable(
|
|
2916
|
+
checkReadable(path, flag, DOCUMENT_LIMIT);
|
|
3916
2917
|
}
|
|
3917
|
-
return readTextFile(
|
|
2918
|
+
return readTextFile(path, flag, DOCUMENT_LIMIT);
|
|
3918
2919
|
}
|
|
3919
|
-
function readTextFile(
|
|
3920
|
-
if (
|
|
2920
|
+
function readTextFile(path, option, limit = TEXT_LIMIT) {
|
|
2921
|
+
if (path === STDIN_PATH) {
|
|
3921
2922
|
let piped;
|
|
3922
2923
|
try {
|
|
3923
2924
|
piped = readFileSync2(0);
|
|
@@ -3929,10 +2930,10 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
|
|
|
3929
2930
|
}
|
|
3930
2931
|
return decodeUtf8(piped, `${option} -`);
|
|
3931
2932
|
}
|
|
3932
|
-
const flag = `${option} ${
|
|
2933
|
+
const flag = `${option} ${path}`;
|
|
3933
2934
|
let bytes;
|
|
3934
2935
|
try {
|
|
3935
|
-
bytes = readFileSync2(
|
|
2936
|
+
bytes = readFileSync2(path);
|
|
3936
2937
|
} catch (error) {
|
|
3937
2938
|
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
3938
2939
|
}
|
|
@@ -3949,11 +2950,11 @@ function decodeUtf8(bytes, flag) {
|
|
|
3949
2950
|
function oversize(what, limit) {
|
|
3950
2951
|
return `${what}: over ${limit.bytes / 1e6} MB \u2014 ${limit.why}`;
|
|
3951
2952
|
}
|
|
3952
|
-
function checkReadable(
|
|
3953
|
-
const flag = `${option} ${
|
|
2953
|
+
function checkReadable(path, option, limit = TEXT_LIMIT) {
|
|
2954
|
+
const flag = `${option} ${path}`;
|
|
3954
2955
|
let stats3;
|
|
3955
2956
|
try {
|
|
3956
|
-
stats3 = statSync3(
|
|
2957
|
+
stats3 = statSync3(path);
|
|
3957
2958
|
} catch (error) {
|
|
3958
2959
|
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
3959
2960
|
}
|
|
@@ -4153,7 +3154,7 @@ async function procedureSave(ctx, jobId, name) {
|
|
|
4153
3154
|
return {
|
|
4154
3155
|
json: pick2(data, ["id", "url"]),
|
|
4155
3156
|
human: `${saved}, but it can't be read back as you.
|
|
4156
|
-
` + labelled([["Workbench",
|
|
3157
|
+
` + labelled([["Workbench", str5(data["url"]) || null]])
|
|
4157
3158
|
};
|
|
4158
3159
|
}
|
|
4159
3160
|
const procedure = procedureJson(data);
|
|
@@ -4216,18 +3217,18 @@ async function startProcedureRun(ctx, request) {
|
|
|
4216
3217
|
return {
|
|
4217
3218
|
json: pick2(data, ["id", "url"]),
|
|
4218
3219
|
human: `${started}, but it can't be read back as you.
|
|
4219
|
-
` + labelled([["Workbench",
|
|
3220
|
+
` + labelled([["Workbench", str5(data["url"]) || null]]) + "\n" + RUN_NOTE
|
|
4220
3221
|
};
|
|
4221
3222
|
}
|
|
4222
3223
|
const job = withheld(data);
|
|
4223
|
-
const waitingOn =
|
|
3224
|
+
const waitingOn = str5(job["waiting_on"]);
|
|
4224
3225
|
return {
|
|
4225
3226
|
json: job,
|
|
4226
3227
|
human: `${started}.
|
|
4227
3228
|
` + labelled([
|
|
4228
|
-
["State",
|
|
4229
|
-
["Workbench",
|
|
4230
|
-
["Approve at",
|
|
3229
|
+
["State", str5(job["state"]) + (waitingOn === "" ? "" : ` \xB7 waiting on ${waitingOn}`)],
|
|
3230
|
+
["Workbench", str5(job["url"]) || null],
|
|
3231
|
+
["Approve at", str5(job["approval_url"]) || null]
|
|
4231
3232
|
]) + "\n" + RUN_NOTE
|
|
4232
3233
|
};
|
|
4233
3234
|
}
|
|
@@ -4269,16 +3270,16 @@ function jsonValue(text, what) {
|
|
|
4269
3270
|
throw new UsageError(`${what}: not valid JSON after := (${firstLine(error)}); quote a string as name=value`);
|
|
4270
3271
|
}
|
|
4271
3272
|
}
|
|
4272
|
-
function readParamsFile(
|
|
4273
|
-
const text = readFileOption("--params-file",
|
|
3273
|
+
function readParamsFile(path) {
|
|
3274
|
+
const text = readFileOption("--params-file", path);
|
|
4274
3275
|
let parsed;
|
|
4275
3276
|
try {
|
|
4276
3277
|
parsed = JSON.parse(text);
|
|
4277
3278
|
} catch (error) {
|
|
4278
|
-
throw new UsageError(`--params-file ${
|
|
3279
|
+
throw new UsageError(`--params-file ${path}: not valid JSON (${firstLine(error)})`);
|
|
4279
3280
|
}
|
|
4280
3281
|
if (!isRecord4(parsed)) {
|
|
4281
|
-
throw new UsageError(`--params-file ${
|
|
3282
|
+
throw new UsageError(`--params-file ${path}: the parameters are one JSON object of names and values`);
|
|
4282
3283
|
}
|
|
4283
3284
|
return parsed;
|
|
4284
3285
|
}
|
|
@@ -4290,24 +3291,24 @@ function bareId(value, flag) {
|
|
|
4290
3291
|
function procedureJson(data) {
|
|
4291
3292
|
const procedure = pick2(data, PROCEDURE_FIELDS);
|
|
4292
3293
|
if ("versions" in procedure) {
|
|
4293
|
-
procedure["versions"] =
|
|
3294
|
+
procedure["versions"] = records3(procedure["versions"]).map((version2) => pick2(version2, PROCEDURE_VERSION_FIELDS));
|
|
4294
3295
|
}
|
|
4295
3296
|
return procedure;
|
|
4296
3297
|
}
|
|
4297
3298
|
function procedureDetail(procedure) {
|
|
4298
3299
|
const current = procedure["current_version"];
|
|
4299
|
-
const versions =
|
|
4300
|
-
(version2) => ` ${clean(`v${
|
|
3300
|
+
const versions = records3(procedure["versions"]).map(
|
|
3301
|
+
(version2) => ` ${clean(`v${str5(version2["version"])}${version2["version"] === current ? " (current)" : ""} ${str5(version2["inserted_at"])} recipe ${str5(version2["recipe_digest"])}`)}
|
|
4301
3302
|
${"Capabilities:".padEnd(15)}${clean(list2(version2["capability_ids"]))}
|
|
4302
3303
|
${"Actions:".padEnd(15)}${clean(list2(version2["action_roster"]))}
|
|
4303
3304
|
`
|
|
4304
3305
|
);
|
|
4305
3306
|
return `Procedure ${clean(procedure["name"])} \xB7 v${clean(current)} \xB7 ${clean(procedure["id"])}
|
|
4306
3307
|
` + labelled([
|
|
4307
|
-
["Saved from", `job ${
|
|
4308
|
-
["Workbench",
|
|
4309
|
-
["Created",
|
|
4310
|
-
["Updated",
|
|
3308
|
+
["Saved from", `job ${str5(procedure["job_id"])}`],
|
|
3309
|
+
["Workbench", str5(procedure["job_url"]) || null],
|
|
3310
|
+
["Created", str5(procedure["inserted_at"]) || null],
|
|
3311
|
+
["Updated", str5(procedure["updated_at"]) || null]
|
|
4311
3312
|
]) + (versions.length === 0 ? "\nVersions: none\n" : "\nVersions:\n" + versions.join("")) + parametersSection(procedure["parameter_schema"]) + "\n" + RUN_NOTE;
|
|
4312
3313
|
}
|
|
4313
3314
|
function parametersSection(schema) {
|
|
@@ -4315,7 +3316,7 @@ function parametersSection(schema) {
|
|
|
4315
3316
|
const lines = JSON.stringify(schema, null, 2).split("\n").map((line) => ` ${clean(line)}
|
|
4316
3317
|
`).join("");
|
|
4317
3318
|
const scoped = isRecord4(schema["scoped_inputs"]) ? schema["scoped_inputs"]["required"] : void 0;
|
|
4318
|
-
const scopes = Array.isArray(scoped) ? scoped.map(
|
|
3319
|
+
const scopes = Array.isArray(scoped) ? scoped.map(str5) : [];
|
|
4319
3320
|
let needs = "";
|
|
4320
3321
|
if (scopes.includes("contact_reconciliation")) {
|
|
4321
3322
|
needs = "Its runs also need --group <contact-group-id> and --source <uploaded-file-id> (contact_reconciliation).\n";
|
|
@@ -4326,18 +3327,18 @@ function parametersSection(schema) {
|
|
|
4326
3327
|
return "\nParameters a run takes (JSON schema):\n" + lines + needs;
|
|
4327
3328
|
}
|
|
4328
3329
|
function list2(value) {
|
|
4329
|
-
return (Array.isArray(value) ? value.map(
|
|
3330
|
+
return (Array.isArray(value) ? value.map(str5).join(", ") : "") || "none";
|
|
4330
3331
|
}
|
|
4331
3332
|
function pick2(record, keys) {
|
|
4332
3333
|
return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
|
|
4333
3334
|
}
|
|
4334
|
-
function
|
|
3335
|
+
function records3(value) {
|
|
4335
3336
|
return Array.isArray(value) ? value.filter(isRecord4) : [];
|
|
4336
3337
|
}
|
|
4337
3338
|
function isRecord4(value) {
|
|
4338
3339
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4339
3340
|
}
|
|
4340
|
-
function
|
|
3341
|
+
function str5(value) {
|
|
4341
3342
|
return value === null || value === void 0 ? "" : String(value);
|
|
4342
3343
|
}
|
|
4343
3344
|
function firstLine(error) {
|
|
@@ -4391,8 +3392,8 @@ function jsonSafe(value) {
|
|
|
4391
3392
|
function firstLine2(error) {
|
|
4392
3393
|
return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
|
|
4393
3394
|
}
|
|
4394
|
-
function readPlanFile(
|
|
4395
|
-
return parsePlanDocument(readFileOption(flag,
|
|
3395
|
+
function readPlanFile(path, flag = "--file") {
|
|
3396
|
+
return parsePlanDocument(readFileOption(flag, path), path);
|
|
4396
3397
|
}
|
|
4397
3398
|
async function planCreate(ctx, plan, opts) {
|
|
4398
3399
|
requireDocument(plan, "a plan is one object: the root's fields and its steps");
|
|
@@ -4405,7 +3406,7 @@ async function planCreate(ctx, plan, opts) {
|
|
|
4405
3406
|
throw explainMissing(error, `no team ${team} here, or this Harness server has no plan API yet`);
|
|
4406
3407
|
}
|
|
4407
3408
|
const created = body.data;
|
|
4408
|
-
const id =
|
|
3409
|
+
const id = str6(created["id"]);
|
|
4409
3410
|
return {
|
|
4410
3411
|
json: created,
|
|
4411
3412
|
human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
|
|
@@ -4511,7 +3512,7 @@ async function gateSet(ctx, issue, gate, satisfied) {
|
|
|
4511
3512
|
}
|
|
4512
3513
|
}
|
|
4513
3514
|
const target = list3 === null ? { id: wanted.toLowerCase(), label: wanted.toLowerCase() } : findGate(list3, issue, wanted, expert);
|
|
4514
|
-
const id =
|
|
3515
|
+
const id = str6(target["id"]);
|
|
4515
3516
|
let body;
|
|
4516
3517
|
try {
|
|
4517
3518
|
body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
|
|
@@ -4602,8 +3603,8 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied, allowLook
|
|
|
4602
3603
|
const message = satisfied ? `Cannot satisfy ${named2}: only ${subject} can tick it` : `Cannot un-satisfy ${named2}: only ${subject} or Harness staff can untick it`;
|
|
4603
3604
|
return new HttpError(error.status, message, error.body, error.retryAfter);
|
|
4604
3605
|
}
|
|
4605
|
-
var gateLabel = (condition) =>
|
|
4606
|
-
var gateKey = (condition) =>
|
|
3606
|
+
var gateLabel = (condition) => str6(condition["label"]).trim();
|
|
3607
|
+
var gateKey = (condition) => str6(condition["key"]);
|
|
4607
3608
|
function exactMatches(conditions, wanted) {
|
|
4608
3609
|
return conditions.filter(
|
|
4609
3610
|
(condition) => condition["kind"] === "gate" && (gateKey(condition) !== "" && gateKey(condition) === wanted || gateLabel(condition) === wanted)
|
|
@@ -4642,7 +3643,7 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4642
3643
|
function isRecord5(value) {
|
|
4643
3644
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4644
3645
|
}
|
|
4645
|
-
function
|
|
3646
|
+
function str6(value) {
|
|
4646
3647
|
return value === null || value === void 0 ? "" : String(value);
|
|
4647
3648
|
}
|
|
4648
3649
|
|
|
@@ -4663,287 +3664,7 @@ function isRecord6(value) {
|
|
|
4663
3664
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4664
3665
|
}
|
|
4665
3666
|
|
|
4666
|
-
// src/core/credentials.ts
|
|
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";
|
|
4669
|
-
import { homedir } from "os";
|
|
4670
|
-
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
4671
|
-
function defaultCredentialsPath(env = process.env) {
|
|
4672
|
-
const xdg = env["XDG_CONFIG_HOME"];
|
|
4673
|
-
const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
|
|
4674
|
-
return join2(base, "harness", "credentials.json");
|
|
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
|
-
}
|
|
4685
|
-
function readCredentials(path2) {
|
|
4686
|
-
try {
|
|
4687
|
-
const parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
4688
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
4689
|
-
const { url, token } = parsed;
|
|
4690
|
-
if (typeof url === "string" && typeof token === "string") return { url, token };
|
|
4691
|
-
}
|
|
4692
|
-
} catch {
|
|
4693
|
-
}
|
|
4694
|
-
return null;
|
|
4695
|
-
}
|
|
4696
|
-
var uniqueCounter = 0;
|
|
4697
|
-
function writeCredentials(path2, url, token) {
|
|
4698
|
-
mkdirSync2(dirname2(path2), { recursive: true });
|
|
4699
|
-
const tempPath = `${path2}.${process.pid}.${++uniqueCounter}.tmp`;
|
|
4700
|
-
try {
|
|
4701
|
-
writeFileSync2(tempPath, "", { mode: 384 });
|
|
4702
|
-
chmodSync2(tempPath, 384);
|
|
4703
|
-
writeFileSync2(tempPath, JSON.stringify({ url, token }));
|
|
4704
|
-
renameSync2(tempPath, path2);
|
|
4705
|
-
} finally {
|
|
4706
|
-
rmSync2(tempPath, { force: true });
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
function deleteCredentials(path2) {
|
|
4710
|
-
rmSync2(path2, { force: true });
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
// src/core/repo-config.ts
|
|
4714
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4715
|
-
import { homedir as homedir2 } from "os";
|
|
4716
|
-
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
|
|
4717
|
-
var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
|
|
4718
|
-
function findRepoConfig(startDir = process.cwd()) {
|
|
4719
|
-
let dir = startDir;
|
|
4720
|
-
const { root } = parse(startDir);
|
|
4721
|
-
for (; ; ) {
|
|
4722
|
-
const candidate = join3(dir, CONFIG_RELATIVE_PATH);
|
|
4723
|
-
if (existsSync(candidate)) {
|
|
4724
|
-
return readRepoConfig(candidate);
|
|
4725
|
-
}
|
|
4726
|
-
if (dir === root) return { config: null, warnings: [] };
|
|
4727
|
-
dir = dirname3(dir);
|
|
4728
|
-
}
|
|
4729
|
-
}
|
|
4730
|
-
function readRepoConfig(path2) {
|
|
4731
|
-
let parsed;
|
|
4732
|
-
try {
|
|
4733
|
-
parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
4734
|
-
} catch (error) {
|
|
4735
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
4736
|
-
return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
|
|
4737
|
-
}
|
|
4738
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4739
|
-
return { config: null, warnings: [`harness: ignoring ${path2} \u2014 expected a JSON object`] };
|
|
4740
|
-
}
|
|
4741
|
-
const record = parsed;
|
|
4742
|
-
const config = { path: path2 };
|
|
4743
|
-
const warnings = [];
|
|
4744
|
-
for (const field3 of ["team", "url", "org"]) {
|
|
4745
|
-
const value = record[field3];
|
|
4746
|
-
if (value === void 0) continue;
|
|
4747
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
4748
|
-
config[field3] = value.trim();
|
|
4749
|
-
} else {
|
|
4750
|
-
warnings.push(`harness: ignoring "${field3}" in ${path2} \u2014 expected a non-empty string`);
|
|
4751
|
-
}
|
|
4752
|
-
}
|
|
4753
|
-
return { config, warnings };
|
|
4754
|
-
}
|
|
4755
|
-
function trustedHostsPath(env = process.env) {
|
|
4756
|
-
const xdg = env["XDG_CONFIG_HOME"];
|
|
4757
|
-
const base = xdg !== void 0 && xdg !== "" && isAbsolute2(xdg) ? xdg : join3(homedir2(), ".config");
|
|
4758
|
-
return join3(base, "harness", "trusted-hosts.json");
|
|
4759
|
-
}
|
|
4760
|
-
function readTrustedHosts(env) {
|
|
4761
|
-
try {
|
|
4762
|
-
const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
|
|
4763
|
-
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
|
|
4764
|
-
return parsed["hosts"].filter(
|
|
4765
|
-
(host) => typeof host === "string"
|
|
4766
|
-
);
|
|
4767
|
-
}
|
|
4768
|
-
} catch {
|
|
4769
|
-
}
|
|
4770
|
-
return [];
|
|
4771
|
-
}
|
|
4772
|
-
function isHostTrusted(baseUrl, env) {
|
|
4773
|
-
return readTrustedHosts(env).includes(baseUrl);
|
|
4774
|
-
}
|
|
4775
|
-
function trustHost(baseUrl, env) {
|
|
4776
|
-
const hosts = readTrustedHosts(env);
|
|
4777
|
-
if (hosts.includes(baseUrl)) return;
|
|
4778
|
-
const path2 = trustedHostsPath(env);
|
|
4779
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
4780
|
-
writeFileSync3(path2, JSON.stringify({ hosts: [...hosts, baseUrl] }, null, 2) + "\n");
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
// src/core/config.ts
|
|
4784
|
-
import { homedir as homedir3, tmpdir } from "os";
|
|
4785
|
-
import { isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
4786
|
-
var DEFAULT_BASE_URL = "https://harness.infra.one";
|
|
4787
|
-
function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
4788
|
-
let warnings = [];
|
|
4789
|
-
if (repo === void 0) {
|
|
4790
|
-
const lookup = findRepoConfig();
|
|
4791
|
-
repo = lookup.config;
|
|
4792
|
-
warnings = lookup.warnings;
|
|
4793
|
-
}
|
|
4794
|
-
const path2 = credentialsPath ?? defaultCredentialsPath(env);
|
|
4795
|
-
const stored = readCredentials(path2);
|
|
4796
|
-
const envUrl = blankToNull(env["HARNESS_API_URL"]);
|
|
4797
|
-
const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
|
|
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
|
-
}
|
|
4803
|
-
let rawUrl;
|
|
4804
|
-
let hostSource;
|
|
4805
|
-
if (envUrl !== null) {
|
|
4806
|
-
rawUrl = envUrl;
|
|
4807
|
-
hostSource = "env";
|
|
4808
|
-
} else if (envToken !== null) {
|
|
4809
|
-
if (repoUrl !== null && normalize(repoUrl) !== normalize(DEFAULT_BASE_URL)) {
|
|
4810
|
-
return {
|
|
4811
|
-
kind: "error",
|
|
4812
|
-
message: `this repo's config (${repo?.path}) targets ${repoUrl}, but HARNESS_API_TOKEN only travels to the host the environment names \u2014 set HARNESS_API_URL explicitly to pair them`,
|
|
4813
|
-
warnings
|
|
4814
|
-
};
|
|
4815
|
-
}
|
|
4816
|
-
rawUrl = DEFAULT_BASE_URL;
|
|
4817
|
-
hostSource = "default";
|
|
4818
|
-
} else if (repoUrl !== null) {
|
|
4819
|
-
rawUrl = repoUrl;
|
|
4820
|
-
hostSource = "repo-config";
|
|
4821
|
-
} else if (org !== void 0) {
|
|
4822
|
-
rawUrl = DEFAULT_BASE_URL;
|
|
4823
|
-
hostSource = "default";
|
|
4824
|
-
} else if (stored !== null) {
|
|
4825
|
-
rawUrl = stored.url;
|
|
4826
|
-
hostSource = "stored-login";
|
|
4827
|
-
} else {
|
|
4828
|
-
rawUrl = DEFAULT_BASE_URL;
|
|
4829
|
-
hostSource = "default";
|
|
4830
|
-
}
|
|
4831
|
-
const validated = validateBaseUrl(rawUrl);
|
|
4832
|
-
if (!validated.ok) return { kind: "error", message: validated.message, warnings };
|
|
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);
|
|
4836
|
-
let token;
|
|
4837
|
-
if (envToken !== null) {
|
|
4838
|
-
token = envToken;
|
|
4839
|
-
} else if (selected !== null && normalize(selected.url) !== null && normalize(selected.url) === normalize(baseUrl)) {
|
|
4840
|
-
token = selected.token;
|
|
4841
|
-
} else {
|
|
4842
|
-
const result = { kind: "needs-login", baseUrl, hostSource, credentialsPath: selectedPath, warnings };
|
|
4843
|
-
if (org !== void 0) result.org = org;
|
|
4844
|
-
if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
|
|
4845
|
-
return result;
|
|
4846
|
-
}
|
|
4847
|
-
return {
|
|
4848
|
-
kind: "ok",
|
|
4849
|
-
config: {
|
|
4850
|
-
baseUrl,
|
|
4851
|
-
token,
|
|
4852
|
-
actor: blankToNull(env["HARNESS_ACTOR"]),
|
|
4853
|
-
// The committed team is the fallback: the environment (and, above it,
|
|
4854
|
-
// the --team flag at the command layer) stays authoritative.
|
|
4855
|
-
team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
|
|
4856
|
-
cacheDir: resolveCacheDir(env),
|
|
4857
|
-
hostSource,
|
|
4858
|
-
credentialsPath: selectedPath
|
|
4859
|
-
},
|
|
4860
|
-
warnings
|
|
4861
|
-
};
|
|
4862
|
-
}
|
|
4863
|
-
function resolveCacheDir(env = process.env) {
|
|
4864
|
-
return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(resolveCacheHome(env), "harness", "attachments");
|
|
4865
|
-
}
|
|
4866
|
-
function absolute(value) {
|
|
4867
|
-
return value !== void 0 && value !== "" && isAbsolute3(value) ? value : null;
|
|
4868
|
-
}
|
|
4869
|
-
function resolveCacheHome(env = process.env) {
|
|
4870
|
-
return absolute(env["XDG_CACHE_HOME"]) ?? join4(homedir3() || tmpdir(), ".cache");
|
|
4871
|
-
}
|
|
4872
|
-
function normalize(url) {
|
|
4873
|
-
let parsed;
|
|
4874
|
-
try {
|
|
4875
|
-
parsed = new URL(url);
|
|
4876
|
-
} catch {
|
|
4877
|
-
return null;
|
|
4878
|
-
}
|
|
4879
|
-
const port = parsed.port !== "" ? parsed.port : parsed.protocol === "https:" ? "443" : parsed.protocol === "http:" ? "80" : "";
|
|
4880
|
-
return JSON.stringify([parsed.protocol, parsed.hostname, port, parsed.pathname.replace(/\/+$/, "")]);
|
|
4881
|
-
}
|
|
4882
|
-
function validateBaseUrl(baseUrl) {
|
|
4883
|
-
let parsed;
|
|
4884
|
-
try {
|
|
4885
|
-
parsed = new URL(baseUrl);
|
|
4886
|
-
} catch {
|
|
4887
|
-
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
4888
|
-
}
|
|
4889
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
4890
|
-
return { ok: false, message: "HARNESS_API_URL must not embed credentials (user:pass@) \u2014 use HARNESS_API_TOKEN" };
|
|
4891
|
-
}
|
|
4892
|
-
if (parsed.search !== "" || parsed.hash !== "") {
|
|
4893
|
-
return { ok: false, message: `HARNESS_API_URL must not carry a query or fragment, got: ${baseUrl}` };
|
|
4894
|
-
}
|
|
4895
|
-
if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
|
|
4896
|
-
if (parsed.protocol === "http:") {
|
|
4897
|
-
const host = parsed.hostname;
|
|
4898
|
-
if (["localhost", "127.0.0.1", "[::1]"].includes(host) || host.endsWith(".localhost")) return { ok: true };
|
|
4899
|
-
return {
|
|
4900
|
-
ok: false,
|
|
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.`
|
|
4902
|
-
};
|
|
4903
|
-
}
|
|
4904
|
-
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
4905
|
-
}
|
|
4906
|
-
function loginHelp(baseUrl, name = invokedName()) {
|
|
4907
|
-
return `Not signed in to ${baseUrl}.
|
|
4908
|
-
|
|
4909
|
-
Interactively, in a terminal (not from inside an agent \u2014 the browser
|
|
4910
|
-
tab needs someone to see it):
|
|
4911
|
-
|
|
4912
|
-
${name} login
|
|
4913
|
-
|
|
4914
|
-
For agents and CI, mint an API token (admin \u2192 API tokens) with the
|
|
4915
|
-
harness:read scope \u2014 plus harness:write only where the workflow
|
|
4916
|
-
creates or closes \u2014 set an expiry, and export it:
|
|
4917
|
-
|
|
4918
|
-
export HARNESS_API_TOKEN=aone_...
|
|
4919
|
-
|
|
4920
|
-
An outside expert uses the pxc_ credential minted in their expert view:
|
|
4921
|
-
export it the same way, or paste it into \`${name} login --no-browser\`.
|
|
4922
|
-
`;
|
|
4923
|
-
}
|
|
4924
|
-
function blankToNull(value) {
|
|
4925
|
-
if (value === void 0 || value.trim() === "") return null;
|
|
4926
|
-
return value;
|
|
4927
|
-
}
|
|
4928
|
-
|
|
4929
3667
|
export {
|
|
4930
|
-
HttpError,
|
|
4931
|
-
UpgradeRequiredError,
|
|
4932
|
-
TransportError,
|
|
4933
|
-
UsageError,
|
|
4934
|
-
LocalError,
|
|
4935
|
-
CommandError,
|
|
4936
|
-
EXPERT_ASSIGNMENTS_PATH,
|
|
4937
|
-
isExpertToken,
|
|
4938
|
-
EXPERT_LAPSED,
|
|
4939
|
-
explainExpertRefusal,
|
|
4940
|
-
SESSION_PATH,
|
|
4941
|
-
isLoginCredential,
|
|
4942
|
-
Client,
|
|
4943
|
-
clean,
|
|
4944
|
-
json,
|
|
4945
|
-
expertSummary,
|
|
4946
|
-
errorDetails,
|
|
4947
3668
|
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
4948
3669
|
IDEMPOTENCY_KEY_FORMAT,
|
|
4949
3670
|
requireIdentifiers,
|
|
@@ -4992,7 +3713,7 @@ export {
|
|
|
4992
3713
|
linkAdd,
|
|
4993
3714
|
linkUpdate,
|
|
4994
3715
|
linkRm,
|
|
4995
|
-
|
|
3716
|
+
labelList,
|
|
4996
3717
|
labelCreate,
|
|
4997
3718
|
teamCreate,
|
|
4998
3719
|
teamList,
|
|
@@ -5068,19 +3789,5 @@ export {
|
|
|
5068
3789
|
gateSet,
|
|
5069
3790
|
conditionList,
|
|
5070
3791
|
requirePlanRef,
|
|
5071
|
-
assignments
|
|
5072
|
-
defaultCredentialsPath,
|
|
5073
|
-
orgCredentialsPath,
|
|
5074
|
-
validOrgId,
|
|
5075
|
-
readCredentials,
|
|
5076
|
-
writeCredentials,
|
|
5077
|
-
deleteCredentials,
|
|
5078
|
-
findRepoConfig,
|
|
5079
|
-
isHostTrusted,
|
|
5080
|
-
trustHost,
|
|
5081
|
-
DEFAULT_BASE_URL,
|
|
5082
|
-
resolveConfig,
|
|
5083
|
-
resolveCacheHome,
|
|
5084
|
-
validateBaseUrl,
|
|
5085
|
-
loginHelp
|
|
3792
|
+
assignments
|
|
5086
3793
|
};
|