@allocator-one/harness-cli 1.15.1 → 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 +256 -38
- 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-L2OWOZ4N.js → chunk-CEEQW6ZV.js} +411 -1556
- package/dist/chunk-W7BVOQQG.js +1448 -0
- package/dist/chunk-WSDFLMO6.js +717 -0
- package/dist/cli.js +1886 -1155
- 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-KVRQBVXI.js → server-TRXBTJSA.js} +37 -19
- package/package.json +4 -3
- package/skills/harness-code/SKILL.md +37 -0
- package/spec/openapi.json +4803 -25
|
@@ -1,989 +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
|
-
["Labels", labelsLine(issue["labels"])],
|
|
335
|
-
["Blocked", blockedLine(issue)],
|
|
336
|
-
["Waiting", waitingLine(issue)],
|
|
337
|
-
["Due", issue["due_date"]],
|
|
338
|
-
["Updated", issue["updated_at"]]
|
|
339
|
-
];
|
|
340
|
-
const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
|
|
341
|
-
`).join("");
|
|
342
|
-
const sections = [
|
|
343
|
-
["Description", issue["description"]],
|
|
344
|
-
["Design", issue["design"]],
|
|
345
|
-
["Acceptance criteria", issue["acceptance_criteria"]],
|
|
346
|
-
["Notes", issue["notes"]]
|
|
347
|
-
];
|
|
348
|
-
const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
|
|
349
|
-
${title}:
|
|
350
|
-
${indent(String(text))}
|
|
351
|
-
`).join("");
|
|
352
|
-
return header + fieldLines + sectionText + conditionsSection(issue["conditions"]) + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
|
|
353
|
-
}
|
|
354
|
-
function activityEntries(entries) {
|
|
355
|
-
if (entries.length === 0) return "No activity.\n";
|
|
356
|
-
return entries.map(activityEntry).join("\n");
|
|
357
|
-
}
|
|
358
|
-
function activityEntry(entry) {
|
|
359
|
-
switch (entry["kind"]) {
|
|
360
|
-
case "comment":
|
|
361
|
-
return activityComment(entry);
|
|
362
|
-
case "state_event":
|
|
363
|
-
return activityStateEvent(entry);
|
|
364
|
-
case "assignee_event":
|
|
365
|
-
return activityAssigneeEvent(entry);
|
|
366
|
-
case "condition_event":
|
|
367
|
-
return activityConditionEvent(entry);
|
|
368
|
-
case "field_event":
|
|
369
|
-
return activityFieldEvent(entry);
|
|
370
|
-
default:
|
|
371
|
-
throw new CommandError(`Unknown activity entry kind: ${clean(entry["kind"])}`);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
function activityComment(entry) {
|
|
375
|
-
const comment2 = requiredRecord(entry["comment"], "activity comment");
|
|
376
|
-
const authorValue = comment2["author"];
|
|
377
|
-
const author = authorValue === null ? "unknown" : optionalLabel(requiredRecord(authorValue, "comment author")["name"], "unknown", "comment author name");
|
|
378
|
-
const deleted = comment2["deleted"];
|
|
379
|
-
if (typeof deleted !== "boolean") throw new CommandError("Invalid comment deleted flag.");
|
|
380
|
-
const body = activityCommentBody(comment2["body"], deleted);
|
|
381
|
-
const text = cleanMultiline(body).split("\n").map((line) => " " + line).join("\n");
|
|
382
|
-
return `[${occurredAt(entry)}] ${clean(author)} (${clean(requiredString(comment2["id"], "comment id"))}):
|
|
383
|
-
${text}
|
|
384
|
-
`;
|
|
385
|
-
}
|
|
386
|
-
function activityCommentBody(value, deleted) {
|
|
387
|
-
if (deleted) return "[deleted comment]";
|
|
388
|
-
if (value === null || value === "") return "[comment has no text]";
|
|
389
|
-
return requiredString(value, "comment body");
|
|
390
|
-
}
|
|
391
|
-
function activityStateEvent(entry) {
|
|
392
|
-
const actor = activityWho(entry);
|
|
393
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
394
|
-
let description;
|
|
395
|
-
switch (entry["action"]) {
|
|
396
|
-
case "created":
|
|
397
|
-
description = `${actor} created the issue in ${stateName(entry["to_state"], "created to_state")}`;
|
|
398
|
-
break;
|
|
399
|
-
case "transition":
|
|
400
|
-
description = `${actor} moved the issue from ${nullableStateName(entry["from_state"], "transition from_state")} to ${stateName(entry["to_state"], "transition to_state")}`;
|
|
401
|
-
break;
|
|
402
|
-
case "milestone":
|
|
403
|
-
description = `${actor} recorded the ${stateName(entry["to_state"], "milestone to_state")} milestone`;
|
|
404
|
-
break;
|
|
405
|
-
default:
|
|
406
|
-
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
407
|
-
}
|
|
408
|
-
return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
|
|
409
|
-
`;
|
|
410
|
-
}
|
|
411
|
-
function activityAssigneeEvent(entry) {
|
|
412
|
-
const actor = activityWho(entry);
|
|
413
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
414
|
-
let description;
|
|
415
|
-
switch (entry["action"]) {
|
|
416
|
-
case "assigned":
|
|
417
|
-
description = `${actor} assigned the issue to ${assigneeName(entry["to_assignee"])}`;
|
|
418
|
-
break;
|
|
419
|
-
case "reassigned":
|
|
420
|
-
description = `${actor} reassigned the issue from ${assigneeName(entry["from_assignee"])} to ${assigneeName(entry["to_assignee"])}`;
|
|
421
|
-
break;
|
|
422
|
-
case "unassigned":
|
|
423
|
-
description = `${actor} unassigned the issue from ${assigneeName(entry["from_assignee"])}`;
|
|
424
|
-
break;
|
|
425
|
-
default:
|
|
426
|
-
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
427
|
-
}
|
|
428
|
-
return `[${occurredAt(entry)}] ${description} - ${provenance}
|
|
429
|
-
`;
|
|
430
|
-
}
|
|
431
|
-
function activityConditionEvent(entry) {
|
|
432
|
-
const actor = activityWho(entry);
|
|
433
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
434
|
-
const condition = conditionSubject(requiredRecord(entry["condition"], "activity condition"));
|
|
435
|
-
let description;
|
|
436
|
-
switch (entry["action"]) {
|
|
437
|
-
case "added":
|
|
438
|
-
description = `${actor} added ${condition}`;
|
|
439
|
-
break;
|
|
440
|
-
case "changed":
|
|
441
|
-
description = `${actor} changed ${condition}`;
|
|
442
|
-
break;
|
|
443
|
-
case "removed":
|
|
444
|
-
description = `${actor} removed ${condition}`;
|
|
445
|
-
break;
|
|
446
|
-
case "satisfied":
|
|
447
|
-
description = `${actor} satisfied ${condition}`;
|
|
448
|
-
break;
|
|
449
|
-
case "unsatisfied":
|
|
450
|
-
description = `${actor} marked ${condition} unsatisfied`;
|
|
451
|
-
break;
|
|
452
|
-
default:
|
|
453
|
-
throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
|
|
454
|
-
}
|
|
455
|
-
return `[${occurredAt(entry)}] ${description} \xB7 ${provenance}
|
|
456
|
-
`;
|
|
457
|
-
}
|
|
458
|
-
function conditionSubject(condition) {
|
|
459
|
-
const label = clean(condition["label"]);
|
|
460
|
-
switch (condition["kind"]) {
|
|
461
|
-
case "gate":
|
|
462
|
-
return label === "" ? "the gate" : `the gate "${label}"`;
|
|
463
|
-
case "not_before": {
|
|
464
|
-
const at = clean(condition["not_before"]);
|
|
465
|
-
return at === "" ? "the not-before date" : `the not-before date ${at}`;
|
|
466
|
-
}
|
|
467
|
-
case "event":
|
|
468
|
-
return label === "" ? "the wait condition" : `the system event "${label}"`;
|
|
469
|
-
default:
|
|
470
|
-
return "the wait condition";
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
function activityFieldEvent(entry) {
|
|
474
|
-
const actor = activityWho(entry);
|
|
475
|
-
const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
|
|
476
|
-
const change = fieldChange(requiredString(entry["field"], "activity field"), entry["from"], entry["to"]);
|
|
477
|
-
return `[${occurredAt(entry)}] ${actor} ${change} \xB7 ${provenance}
|
|
478
|
-
`;
|
|
479
|
-
}
|
|
480
|
-
function fieldChange(field3, from, to) {
|
|
481
|
-
switch (field3) {
|
|
482
|
-
case "priority":
|
|
483
|
-
return `changed priority from ${priorityName(from)} to ${priorityName(to)}`;
|
|
484
|
-
case "title":
|
|
485
|
-
return `renamed the issue from \u201C${activityTitle(from)}\u201D to \u201C${activityTitle(to)}\u201D`;
|
|
486
|
-
case "description":
|
|
487
|
-
return "edited the description";
|
|
488
|
-
case "labels":
|
|
489
|
-
return labelsChange(to);
|
|
490
|
-
case "parent":
|
|
491
|
-
return parentChange(from, to);
|
|
492
|
-
case "archived":
|
|
493
|
-
if (typeof to !== "boolean") throw new CommandError("Invalid activity archived value.");
|
|
494
|
-
return to ? "archived the issue" : "unarchived the issue";
|
|
495
|
-
default:
|
|
496
|
-
return `changed the ${clean(field3)} field`;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
var ACTIVITY_PRIORITIES = {
|
|
500
|
-
0: "No priority",
|
|
501
|
-
1: "Urgent",
|
|
502
|
-
2: "High",
|
|
503
|
-
3: "Medium",
|
|
504
|
-
4: "Low"
|
|
505
|
-
};
|
|
506
|
-
function priorityName(value) {
|
|
507
|
-
if (typeof value !== "number" || !Number.isInteger(value)) throw new CommandError("Invalid activity priority.");
|
|
508
|
-
return ACTIVITY_PRIORITIES[value] ?? String(value);
|
|
509
|
-
}
|
|
510
|
-
function activityTitle(value) {
|
|
511
|
-
if (typeof value !== "string") throw new CommandError("Invalid activity title.");
|
|
512
|
-
return clean(value);
|
|
513
|
-
}
|
|
514
|
-
function labelsChange(to) {
|
|
515
|
-
const change = requiredRecord(to, "activity labels");
|
|
516
|
-
const added = labelNames(change["added"]);
|
|
517
|
-
const removed = labelNames(change["removed"]);
|
|
518
|
-
const parts = [
|
|
519
|
-
...added.length > 0 ? [`added ${labelList(added)}`] : [],
|
|
520
|
-
...removed.length > 0 ? [`removed ${labelList(removed)}`] : []
|
|
521
|
-
];
|
|
522
|
-
return parts.length === 0 ? "changed the labels" : parts.join(" and ");
|
|
523
|
-
}
|
|
524
|
-
function labelNames(value) {
|
|
525
|
-
if (!Array.isArray(value)) throw new CommandError("Invalid activity labels.");
|
|
526
|
-
return value.map((label) => clean(requiredString(requiredRecord(label, "activity labels")["name"], "activity labels")));
|
|
527
|
-
}
|
|
528
|
-
function labelList(names) {
|
|
529
|
-
return `${names.length === 1 ? "label" : "labels"} ${names.join(", ")}`;
|
|
530
|
-
}
|
|
531
|
-
function parentChange(from, to) {
|
|
532
|
-
if (from === null && to === null) return "changed the parent";
|
|
533
|
-
if (from === null) return `set the parent to ${parentName(to)}`;
|
|
534
|
-
if (to === null) return `removed the parent ${parentName(from)}`;
|
|
535
|
-
return `changed the parent from ${parentName(from)} to ${parentName(to)}`;
|
|
536
|
-
}
|
|
537
|
-
function parentName(value) {
|
|
538
|
-
const parent = requiredRecord(value, "activity parent");
|
|
539
|
-
return optionalLabel(parent["identifier"], "an unknown issue", "activity parent identifier");
|
|
540
|
-
}
|
|
541
|
-
function activityWho(entry) {
|
|
542
|
-
const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
|
|
543
|
-
const label = entry["actor_label"];
|
|
544
|
-
if (label === void 0 || label === null || label === "") return actor;
|
|
545
|
-
if (typeof label !== "string") throw new CommandError("Invalid activity actor label.");
|
|
546
|
-
const cleaned = clean(label);
|
|
547
|
-
return cleaned === "" ? actor : `${actor} via \u201C${cleaned}\u201D`;
|
|
548
|
-
}
|
|
549
|
-
function occurredAt(entry) {
|
|
550
|
-
return clean(requiredString(entry["occurred_at"], "activity occurrence time"));
|
|
551
|
-
}
|
|
552
|
-
function assigneeName(value) {
|
|
553
|
-
const assignee = requiredRecord(value, "activity assignee");
|
|
554
|
-
if (assignee["kind"] === "unknown") return "Unknown assignee";
|
|
555
|
-
return activityActor(assignee);
|
|
556
|
-
}
|
|
557
|
-
function activityActor(actor) {
|
|
558
|
-
switch (actor["kind"]) {
|
|
559
|
-
case "user":
|
|
560
|
-
return optionalLabel(actor["name"], "Unknown actor", "activity actor name");
|
|
561
|
-
case "system":
|
|
562
|
-
return "System";
|
|
563
|
-
case "deleted_user":
|
|
564
|
-
return "Deleted user";
|
|
565
|
-
case "unknown":
|
|
566
|
-
return "Unknown actor";
|
|
567
|
-
default:
|
|
568
|
-
throw new CommandError(`Unknown activity actor kind: ${clean(actor["kind"])}`);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
function activityProvenance(provenance) {
|
|
572
|
-
switch (provenance["kind"]) {
|
|
573
|
-
case "live":
|
|
574
|
-
case "import":
|
|
575
|
-
case "backfill":
|
|
576
|
-
return String(provenance["kind"]);
|
|
577
|
-
default:
|
|
578
|
-
throw new CommandError(`Unknown activity provenance: ${clean(provenance["kind"])}`);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
function stateName(value, field3) {
|
|
582
|
-
return optionalLabel(requiredRecord(value, field3)["name"], "Unknown state", `${field3} name`);
|
|
583
|
-
}
|
|
584
|
-
function nullableStateName(value, field3) {
|
|
585
|
-
return value === null ? "Unknown state" : stateName(value, field3);
|
|
586
|
-
}
|
|
587
|
-
function optionalLabel(value, fallback, field3) {
|
|
588
|
-
if (value === null || value === "") return fallback;
|
|
589
|
-
return clean(requiredString(value, field3));
|
|
590
|
-
}
|
|
591
|
-
function requiredRecord(value, field3) {
|
|
592
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
593
|
-
throw new CommandError(`Invalid ${field3}.`);
|
|
594
|
-
}
|
|
595
|
-
return value;
|
|
596
|
-
}
|
|
597
|
-
function requiredString(value, field3) {
|
|
598
|
-
if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${field3}.`);
|
|
599
|
-
return value;
|
|
600
|
-
}
|
|
601
|
-
function stats(data) {
|
|
602
|
-
const teams = data["teams"] ?? [];
|
|
603
|
-
const totals = data["totals"] ?? {};
|
|
604
|
-
const rows = teams.map((team) => [
|
|
605
|
-
str(path(team, "team", "key")),
|
|
606
|
-
str(team["open"]),
|
|
607
|
-
str(team["started"]),
|
|
608
|
-
str(team["blocked"]),
|
|
609
|
-
str(team["ready"]),
|
|
610
|
-
str(team["closed"])
|
|
611
|
-
]);
|
|
612
|
-
const totalsRow = ["TOTAL", ...["open", "started", "blocked", "ready", "closed"].map((key) => str(totals[key]))];
|
|
613
|
-
return table(["TEAM", "OPEN", "STARTED", "BLOCKED", "READY", "CLOSED"], [...rows, totalsRow]);
|
|
614
|
-
}
|
|
615
|
-
function table(headers, rows) {
|
|
616
|
-
const all = [headers, ...rows.map((row) => row.map((cell) => clean(cell)))];
|
|
617
|
-
const widths = headers.map((_, column) => Math.max(...all.map((row) => graphemeLength(row[column] ?? ""))));
|
|
618
|
-
return all.map(
|
|
619
|
-
(row) => row.map((cell, column) => padGraphemes(cell, widths[column] ?? 0)).join(" ").replace(/\s+$/, "") + "\n"
|
|
620
|
-
).join("");
|
|
621
|
-
}
|
|
622
|
-
function records(value) {
|
|
623
|
-
if (!Array.isArray(value)) return [];
|
|
624
|
-
return value.filter(
|
|
625
|
-
(entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
function waitingLine(issue) {
|
|
629
|
-
if (issue["waiting_on_condition"] !== true) return null;
|
|
630
|
-
const conditions = records(issue["conditions"]);
|
|
631
|
-
const open = conditions.filter((condition) => condition["satisfied"] !== true).length;
|
|
632
|
-
return `yes, on ${open} of ${conditions.length} wait condition${conditions.length === 1 ? "" : "s"}`;
|
|
633
|
-
}
|
|
634
|
-
function conditionsSection(conditions) {
|
|
635
|
-
const list3 = records(conditions);
|
|
636
|
-
if (list3.length === 0) return "";
|
|
637
|
-
return "\nWait conditions:\n" + list3.map((condition) => ` ${conditionLine(condition)}
|
|
638
|
-
`).join("");
|
|
639
|
-
}
|
|
640
|
-
function conditionLine(condition, options = {}) {
|
|
641
|
-
const satisfied = condition["satisfied"] === true;
|
|
642
|
-
let what;
|
|
643
|
-
switch (condition["kind"]) {
|
|
644
|
-
case "gate": {
|
|
645
|
-
const owner = condition["owner"];
|
|
646
|
-
let ownedBy = "";
|
|
647
|
-
if (typeof owner === "object" && owner !== null) {
|
|
648
|
-
const record = owner;
|
|
649
|
-
ownedBy = options.expertView === true && record["kind"] === "expert" ? "you" : ownerText(record);
|
|
650
|
-
}
|
|
651
|
-
what = `gate ${gateName(condition)}${ownedBy === "" ? "" : ` owned by ${ownedBy}`}`;
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
case "not_before":
|
|
655
|
-
what = `not before ${clean(condition["not_before"])}`;
|
|
656
|
-
break;
|
|
657
|
-
case "event":
|
|
658
|
-
what = "system event";
|
|
659
|
-
break;
|
|
660
|
-
default:
|
|
661
|
-
what = clean(condition["kind"]) || "condition";
|
|
662
|
-
}
|
|
663
|
-
let when = "";
|
|
664
|
-
if (satisfied && typeof condition["satisfied_at"] === "string") {
|
|
665
|
-
let by = clean(path(condition, "satisfied_by", "name"));
|
|
666
|
-
if (by === "" && condition["satisfied_by_kind"] === "expert") by = "an outside expert";
|
|
667
|
-
let role = typeof condition["satisfied_as_role"] === "string" ? roleName(condition["satisfied_as_role"]) : "";
|
|
668
|
-
switch (path(condition, "satisfied_by", "kind")) {
|
|
669
|
-
case "you":
|
|
670
|
-
by = "you";
|
|
671
|
-
break;
|
|
672
|
-
case "role": {
|
|
673
|
-
const as = path(condition, "satisfied_by", "role");
|
|
674
|
-
if (typeof as === "string" && as !== "") role = roleName(as);
|
|
675
|
-
break;
|
|
676
|
-
}
|
|
677
|
-
case "expert":
|
|
678
|
-
by = "an outside expert";
|
|
679
|
-
break;
|
|
680
|
-
case "member":
|
|
681
|
-
by = "a member";
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
when = ` (satisfied ${clean(condition["satisfied_at"])}${by === "" ? "" : ` by ${by}`}${role === "" ? "" : ` as ${role}`})`;
|
|
685
|
-
}
|
|
686
|
-
return `${satisfied ? "[x]" : "[ ]"} ${what}${when} \xB7 ${clean(condition["id"])}`;
|
|
687
|
-
}
|
|
688
|
-
function gateName(gate) {
|
|
689
|
-
const key = clean(gate["key"]);
|
|
690
|
-
return `"${clean(gate["label"])}"${key === "" ? "" : ` [${key}]`}`;
|
|
691
|
-
}
|
|
692
|
-
function ownerText(owner) {
|
|
693
|
-
const label = clean(owner["label"]) || (owner["kind"] === "role" && typeof owner["role"] === "string" ? roleName(owner["role"]) : "");
|
|
694
|
-
const ref = clean(owner["ref"]);
|
|
695
|
-
if (owner["kind"] === "expert") {
|
|
696
|
-
const email = label || ref.replace(/^expert:/, "");
|
|
697
|
-
return email === "" ? "" : `${email} (expert)`;
|
|
698
|
-
}
|
|
699
|
-
if (label === "") return ref;
|
|
700
|
-
return ref === "" || ref === label ? label : `${label} (${ref})`;
|
|
701
|
-
}
|
|
702
|
-
function expertSummary(seat) {
|
|
703
|
-
const role = typeof seat.role === "string" && seat.role !== "" ? roleName(seat.role) : "Outside expert";
|
|
704
|
-
const status = clean(seat.plan?.status);
|
|
705
|
-
const plan = seat.plan === null ? ", no plan" : ` on plan ${clean(seat.plan.id)}${status === "" ? "" : ` (${status})`}`;
|
|
706
|
-
const steps = seat.assigned_steps;
|
|
707
|
-
const count = steps === 0 ? "no assigned steps" : `${steps} assigned step${steps === 1 ? "" : "s"}`;
|
|
708
|
-
return `${role}${plan} \xB7 ${count}`;
|
|
709
|
-
}
|
|
710
|
-
function expertAssignments(steps) {
|
|
711
|
-
return steps.map((step) => "\n" + assignedStep(step)).join("");
|
|
712
|
-
}
|
|
713
|
-
function assignedStep(step) {
|
|
714
|
-
const blockers = typeof step["open_blockers"] === "number" ? step["open_blockers"] : 0;
|
|
715
|
-
const fields = [
|
|
716
|
-
["State", path(step, "state", "name")],
|
|
717
|
-
["Due", step["due_date"]],
|
|
718
|
-
["Blocked", blockers > 0 ? `yes, by ${blockers} open issue${blockers === 1 ? "" : "s"}` : null]
|
|
719
|
-
];
|
|
720
|
-
const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
|
|
721
|
-
`).join("");
|
|
722
|
-
const gates = records(step["gates"]);
|
|
723
|
-
const gateLines = gates.length === 0 ? " Gates: none of yours\n" : " Gates:\n" + gates.map((gate) => ` ${conditionLine(gate, { expertView: true })}
|
|
724
|
-
`).join("");
|
|
725
|
-
const description = step["description"];
|
|
726
|
-
const descriptionLines = typeof description === "string" && description !== "" ? " Description:\n" + indent(indent(description)) + "\n" : "";
|
|
727
|
-
return `${clean(step["identifier"])} \xB7 ${clean(step["title"])}
|
|
728
|
-
${fieldLines}${gateLines}${descriptionLines}`;
|
|
729
|
-
}
|
|
730
|
-
var ROLE_NAMES = { gp: "GP" };
|
|
731
|
-
function roleName(role) {
|
|
732
|
-
const slug = clean(role);
|
|
733
|
-
if (Object.hasOwn(ROLE_NAMES, slug)) return ROLE_NAMES[slug];
|
|
734
|
-
const words = slug.replace(/_/g, " ");
|
|
735
|
-
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
736
|
-
}
|
|
737
|
-
var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
|
|
738
|
-
function planDetail(plan) {
|
|
739
|
-
const steps = records(plan["steps"]);
|
|
740
|
-
const fields = [
|
|
741
|
-
["Root", planRootLine(plan["root"])],
|
|
742
|
-
["Revision", planRevisionLine(plan)],
|
|
743
|
-
["Budgets", planBudgetsLine(plan)],
|
|
744
|
-
["Estimate", planEstimateLine(plan["estimate"])]
|
|
745
|
-
];
|
|
746
|
-
const fieldLines = fields.filter((field3) => field3[1] !== null).map(([label, value]) => ` ${(label + ":").padEnd(12)}${value}
|
|
747
|
-
`).join("");
|
|
748
|
-
return `Plan ${clean(plan["id"])} \xB7 ${clean(plan["status"])}
|
|
749
|
-
` + fieldLines + planStepsSection(steps, str(plan["status"])) + planEdgesSection(plan["edges"]) + planConditionsSection(steps) + planWaitingSection(steps) + planHeldSection(plan["held_steps"]);
|
|
750
|
-
}
|
|
751
|
-
function planRootLine(root) {
|
|
752
|
-
if (typeof root !== "object" || root === null) return null;
|
|
753
|
-
const record = root;
|
|
754
|
-
return `${clean(record["identifier"])} \xB7 ${clean(record["title"])}`;
|
|
755
|
-
}
|
|
756
|
-
function planRevisionLine(plan) {
|
|
757
|
-
const revision = plan["revision"];
|
|
758
|
-
if (typeof revision !== "object" || revision === null) return "none yet (never released)";
|
|
759
|
-
const number = clean(revision["number"]);
|
|
760
|
-
const by = clean(path(plan, "released_by", "name"));
|
|
761
|
-
const at = clean(plan["released_at"]);
|
|
762
|
-
if (by === "" && at === "") return number;
|
|
763
|
-
return `${number} \xB7 released${by === "" ? "" : ` by ${by}`}${at === "" ? "" : ` at ${at}`}`;
|
|
764
|
-
}
|
|
765
|
-
function planBudgetsLine(plan) {
|
|
766
|
-
const retries = plan["retry_budget"];
|
|
767
|
-
const retryText = typeof retries === "number" ? `${retries} ${retries === 1 ? "retry" : "retries"} per failed Operator step` : null;
|
|
768
|
-
const spend = plan["spend_budget_cents"];
|
|
769
|
-
const spendText = typeof spend === "number" ? `spend budget ${spend} cents` : "no spend budget";
|
|
770
|
-
return [retryText, spendText].filter((part) => part !== null).join(" \xB7 ");
|
|
771
|
-
}
|
|
772
|
-
function planEstimateLine(estimate) {
|
|
773
|
-
if (typeof estimate !== "object" || estimate === null) return null;
|
|
774
|
-
const record = estimate;
|
|
775
|
-
const ceiling = record["ceiling_cents"];
|
|
776
|
-
if (typeof ceiling !== "number") return null;
|
|
777
|
-
const minimum = record["minimum_budget_cents"];
|
|
778
|
-
return `up to ${ceiling} cents${typeof minimum === "number" ? ` (minimum budget ${minimum} cents)` : ""}`;
|
|
779
|
-
}
|
|
780
|
-
function planStepsSection(steps, planStatus) {
|
|
781
|
-
if (steps.length === 0) return "";
|
|
782
|
-
const depths = stepDepths(steps);
|
|
783
|
-
const rows = steps.map((step) => [
|
|
784
|
-
" ".repeat(depths.get(str(step["identifier"])) ?? 0) + str(step["identifier"]),
|
|
785
|
-
str(step["key"]) || "-",
|
|
786
|
-
str(step["executor"]) || "-",
|
|
787
|
-
str(path(step, "state", "name")) || "-",
|
|
788
|
-
stepStatus(step, planStatus),
|
|
789
|
-
truncate(step["title"])
|
|
790
|
-
]);
|
|
791
|
-
const rendered = table(["STEP", "KEY", "EXECUTOR", "STATE", "STATUS", "TITLE"], rows);
|
|
792
|
-
return "\nSteps:\n" + indentLines(rendered);
|
|
793
|
-
}
|
|
794
|
-
function stepDepths(steps) {
|
|
795
|
-
const parents = new Map(steps.map((step) => [str(step["identifier"]), str(step["parent"])]));
|
|
796
|
-
const depths = /* @__PURE__ */ new Map();
|
|
797
|
-
for (const identifier of parents.keys()) {
|
|
798
|
-
let depth = 0;
|
|
799
|
-
let parent = parents.get(identifier) ?? "";
|
|
800
|
-
const seen = /* @__PURE__ */ new Set([identifier]);
|
|
801
|
-
while (parent !== "" && parents.has(parent) && !seen.has(parent)) {
|
|
802
|
-
seen.add(parent);
|
|
803
|
-
depth += 1;
|
|
804
|
-
parent = parents.get(parent) ?? "";
|
|
805
|
-
}
|
|
806
|
-
depths.set(identifier, depth);
|
|
807
|
-
}
|
|
808
|
-
return depths;
|
|
809
|
-
}
|
|
810
|
-
function stepStatus(step, planStatus) {
|
|
811
|
-
const stateType = path(step, "state", "type");
|
|
812
|
-
if (step["archived"] === true) return "archived";
|
|
813
|
-
if (stateType === "completed") return "done";
|
|
814
|
-
if (stateType === "canceled") return "cancelled";
|
|
815
|
-
if (step["container"] === true) return "container";
|
|
816
|
-
if (step["ready"] === true) return "ready";
|
|
817
|
-
if (step["held"] === true) return "held";
|
|
818
|
-
if (step["in_revision"] === false && LIVE_PLAN_STATUSES.includes(planStatus)) return "not in revision";
|
|
819
|
-
if (records(step["conditions"]).some((condition) => condition["satisfied"] !== true)) return "waiting";
|
|
820
|
-
return "-";
|
|
821
|
-
}
|
|
822
|
-
function planEdgesSection(edges) {
|
|
823
|
-
const list3 = records(edges);
|
|
824
|
-
if (list3.length === 0) return "";
|
|
825
|
-
return "\nEdges (blocker \u2192 blocked):\n" + list3.map((edge) => ` ${clean(edge["from"])} \u2192 ${clean(edge["to"])}
|
|
826
|
-
`).join("");
|
|
827
|
-
}
|
|
828
|
-
function planConditionsSection(steps) {
|
|
829
|
-
const lines = steps.flatMap(
|
|
830
|
-
(step) => records(step["conditions"]).map((condition) => [clean(step["identifier"]), conditionLine(condition)])
|
|
831
|
-
);
|
|
832
|
-
if (lines.length === 0) return "";
|
|
833
|
-
const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
|
|
834
|
-
return "\nWait conditions:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
|
|
835
|
-
`).join("");
|
|
836
|
-
}
|
|
837
|
-
function planWaitingSection(steps) {
|
|
838
|
-
const lines = steps.flatMap(
|
|
839
|
-
(step) => records(step["waiting_for"]).map((wait) => [clean(step["identifier"]), waitLine(wait)])
|
|
840
|
-
);
|
|
841
|
-
if (lines.length === 0) return "";
|
|
842
|
-
const width = Math.max(...lines.map(([identifier]) => graphemeLength(identifier)));
|
|
843
|
-
return "\nWaiting for:\n" + lines.map(([identifier, line]) => ` ${padGraphemes(identifier, width)} ${line}
|
|
844
|
-
`).join("");
|
|
845
|
-
}
|
|
846
|
-
function waitLine(wait) {
|
|
847
|
-
const name = gateName(wait);
|
|
848
|
-
const owner = wait["owner"];
|
|
849
|
-
if (typeof owner !== "object" || owner === null) return `${name} \xB7 no owner, anyone with standing can tick`;
|
|
850
|
-
const holders = wait["holders"];
|
|
851
|
-
let who;
|
|
852
|
-
if (!Array.isArray(holders)) who = "who can tick is not known";
|
|
853
|
-
else if (holders.length === 0) who = "nobody can tick";
|
|
854
|
-
else who = `can tick: ${records(holders).map(holderText).join(", ")}`;
|
|
855
|
-
return `${name} \xB7 ${ownerText(owner)} \xB7 ${who}`;
|
|
856
|
-
}
|
|
857
|
-
function holderText(holder) {
|
|
858
|
-
const name = clean(holder["name"]);
|
|
859
|
-
if (holder["kind"] !== "expert") return name;
|
|
860
|
-
const organization = clean(holder["organization"]);
|
|
861
|
-
return `${name} (expert${organization === "" ? "" : `, ${organization}`})`;
|
|
862
|
-
}
|
|
863
|
-
function planHeldSection(heldSteps) {
|
|
864
|
-
if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
|
|
865
|
-
return `
|
|
866
|
-
Held by an edit until the plan is released again: ${heldSteps.map((step) => clean(step)).join(", ")}
|
|
867
|
-
`;
|
|
868
|
-
}
|
|
869
|
-
function indentLines(text) {
|
|
870
|
-
return text.split("\n").map((line) => line === "" ? line : " " + line).join("\n");
|
|
871
|
-
}
|
|
872
|
-
function errorDetails(body) {
|
|
873
|
-
return changesetDetails(body["details"]) + violationDetails(body["violations"]);
|
|
874
|
-
}
|
|
875
|
-
function changesetDetails(details) {
|
|
876
|
-
if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
|
|
877
|
-
return Object.entries(details).map(([field3, messages]) => {
|
|
878
|
-
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
879
|
-
return `
|
|
880
|
-
${clean(field3)}: ${clean(text)}`;
|
|
881
|
-
}).join("");
|
|
882
|
-
}
|
|
883
|
-
function violationDetails(violations) {
|
|
884
|
-
return records(violations).map((violation) => {
|
|
885
|
-
const code = clean(violation["code"]);
|
|
886
|
-
const message = clean(violation["message"]) || code || "invalid";
|
|
887
|
-
return `
|
|
888
|
-
${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
|
|
889
|
-
}).join("");
|
|
890
|
-
}
|
|
891
|
-
function violationSubject(violation) {
|
|
892
|
-
const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
|
|
893
|
-
const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
|
|
894
|
-
const issue = typeof violation["issue"] === "string" ? clean(violation["issue"]) : null;
|
|
895
|
-
if (key !== null) {
|
|
896
|
-
const where = index ?? issue;
|
|
897
|
-
return where === null ? `step "${key}"` : `step "${key}" (${where})`;
|
|
898
|
-
}
|
|
899
|
-
return issue ?? index ?? "plan";
|
|
900
|
-
}
|
|
901
|
-
function relationsSection(relations) {
|
|
902
|
-
if (!Array.isArray(relations) || relations.length === 0) return "";
|
|
903
|
-
const lines = relations.map((relation) => ` ${str(relation["direction"]).padEnd(14)}${str(relation["other_issue"])}
|
|
904
|
-
`).join("");
|
|
905
|
-
return "\nRelations:\n" + lines;
|
|
906
|
-
}
|
|
907
|
-
function childrenSection(children) {
|
|
908
|
-
if (!Array.isArray(children) || children.length === 0) return "";
|
|
909
|
-
const lines = children.map((child) => {
|
|
910
|
-
const state = str(path(child, "state", "name")) || "-";
|
|
911
|
-
return ` ${str(child["identifier"])} [${state}] ${truncate(child["title"])}
|
|
912
|
-
`;
|
|
913
|
-
}).join("");
|
|
914
|
-
return "\nSub-issues:\n" + lines;
|
|
915
|
-
}
|
|
916
|
-
function commentsSection(comments2) {
|
|
917
|
-
if (!Array.isArray(comments2) || comments2.length === 0) return "";
|
|
918
|
-
const lines = comments2.map((comment2) => {
|
|
919
|
-
const author = clean(path(comment2, "author", "name") ?? "unknown");
|
|
920
|
-
return ` [${str(comment2["inserted_at"])}] ${author}: ${truncate(comment2["body"])}
|
|
921
|
-
`;
|
|
922
|
-
}).join("");
|
|
923
|
-
return "\nComments:\n" + lines;
|
|
924
|
-
}
|
|
925
|
-
function imagesSection(images) {
|
|
926
|
-
if (images.length === 0) return "";
|
|
927
|
-
const lines = images.map((image) => ` [${image["source"]}] ${clean(image["path"])}
|
|
928
|
-
`).join("");
|
|
929
|
-
return "\nImages (attachments downloaded locally \u2014 read them, they are part of the issue):\n" + lines;
|
|
930
|
-
}
|
|
931
|
-
function labelsLine(labels) {
|
|
932
|
-
if (!Array.isArray(labels) || labels.length === 0) return null;
|
|
933
|
-
return labels.map((label) => clean(label["name"])).join(", ");
|
|
934
|
-
}
|
|
935
|
-
function blockedLine(issue) {
|
|
936
|
-
if (issue["blocked"] === true && Array.isArray(issue["blocked_by"])) {
|
|
937
|
-
const notes = blockerNotes(issue["blockers"]);
|
|
938
|
-
const blockers = issue["blocked_by"].map((blocker) => {
|
|
939
|
-
const identifier = String(blocker);
|
|
940
|
-
const note = notes.get(identifier);
|
|
941
|
-
return note === void 0 ? identifier : `${identifier} (${note})`;
|
|
942
|
-
});
|
|
943
|
-
return `yes, by ${blockers.join(", ")}`;
|
|
944
|
-
}
|
|
945
|
-
return null;
|
|
946
|
-
}
|
|
947
|
-
function blockerNotes(blockers) {
|
|
948
|
-
const notes = /* @__PURE__ */ new Map();
|
|
949
|
-
if (!Array.isArray(blockers)) return notes;
|
|
950
|
-
for (const blocker of blockers) {
|
|
951
|
-
if (typeof blocker !== "object" || blocker === null) continue;
|
|
952
|
-
const record = blocker;
|
|
953
|
-
if (typeof record["identifier"] !== "string") continue;
|
|
954
|
-
const reasons = [];
|
|
955
|
-
if (record["state_type"] === "canceled") reasons.push("cancelled");
|
|
956
|
-
if (record["archived"] === true) reasons.push("archived");
|
|
957
|
-
if (reasons.length > 0) notes.set(record["identifier"], reasons.join(", "));
|
|
958
|
-
}
|
|
959
|
-
return notes;
|
|
960
|
-
}
|
|
961
|
-
function indent(text) {
|
|
962
|
-
return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
|
|
963
|
-
}
|
|
964
|
-
function cleanMultiline(text) {
|
|
965
|
-
return text.split("\n").map((line) => clean(line)).join("\n");
|
|
966
|
-
}
|
|
967
|
-
function truncate(text) {
|
|
968
|
-
if (text === null || text === void 0) return "";
|
|
969
|
-
const cleaned = clean(text);
|
|
970
|
-
const pieces = graphemes(cleaned);
|
|
971
|
-
if (pieces.length > TITLE_WIDTH) {
|
|
972
|
-
return pieces.slice(0, TITLE_WIDTH - 1).join("") + "\u2026";
|
|
973
|
-
}
|
|
974
|
-
return cleaned;
|
|
975
|
-
}
|
|
976
|
-
function str(value) {
|
|
977
|
-
return value === null || value === void 0 ? "" : String(value);
|
|
978
|
-
}
|
|
979
|
-
function path(record, ...keys) {
|
|
980
|
-
let current = record;
|
|
981
|
-
for (const key of keys) {
|
|
982
|
-
if (typeof current !== "object" || current === null) return null;
|
|
983
|
-
current = current[key];
|
|
984
|
-
}
|
|
985
|
-
return current ?? null;
|
|
986
|
-
}
|
|
37
|
+
} from "./chunk-5THYIKLW.js";
|
|
987
38
|
|
|
988
39
|
// src/commands/issues.ts
|
|
989
40
|
import { openAsBlob, openSync, closeSync, readFileSync, statSync as statSync2 } from "fs";
|
|
@@ -1051,14 +102,14 @@ async function fetchImages(client, issue, cacheDir) {
|
|
|
1051
102
|
return { images, failures };
|
|
1052
103
|
}
|
|
1053
104
|
async function download(client, ref, cacheDir) {
|
|
1054
|
-
const
|
|
105
|
+
const path = join(cacheDir, hostSegment(client.baseUrl), ref.attachmentId, ref.file);
|
|
1055
106
|
try {
|
|
1056
|
-
if (statSync(
|
|
107
|
+
if (statSync(path).size > 0) return { ok: true, path };
|
|
1057
108
|
} catch {
|
|
1058
109
|
}
|
|
1059
110
|
const bytes = await fetchBytes(client, ref);
|
|
1060
111
|
if (!bytes.ok) return bytes;
|
|
1061
|
-
return write(
|
|
112
|
+
return write(path, bytes.bytes);
|
|
1062
113
|
}
|
|
1063
114
|
function hostSegment(baseUrl) {
|
|
1064
115
|
let host = "unknown-host";
|
|
@@ -1098,25 +149,25 @@ function isImage(bytes) {
|
|
|
1098
149
|
return false;
|
|
1099
150
|
}
|
|
1100
151
|
var partialCounter = 0;
|
|
1101
|
-
function write(
|
|
1102
|
-
const partial = `${
|
|
152
|
+
function write(path, bytes) {
|
|
153
|
+
const partial = `${path}.partial.${process.pid}.${++partialCounter}`;
|
|
1103
154
|
try {
|
|
1104
|
-
mkdirSync(dirname(
|
|
1105
|
-
chmodSync(dirname(
|
|
155
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
156
|
+
chmodSync(dirname(path), 448);
|
|
1106
157
|
writeFileSync(partial, bytes);
|
|
1107
158
|
chmodSync(partial, 384);
|
|
1108
|
-
renameSync(partial,
|
|
1109
|
-
return { ok: true, path
|
|
159
|
+
renameSync(partial, path);
|
|
160
|
+
return { ok: true, path };
|
|
1110
161
|
} catch (error) {
|
|
1111
162
|
rmSync(partial, { force: true });
|
|
1112
163
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1113
|
-
return { ok: false, message: `cannot write ${
|
|
164
|
+
return { ok: false, message: `cannot write ${path}: ${detail}` };
|
|
1114
165
|
}
|
|
1115
166
|
}
|
|
1116
167
|
|
|
1117
168
|
// src/core/pagination.ts
|
|
1118
|
-
function notices(meta, requestedPageSize,
|
|
1119
|
-
return capNotice(meta, requestedPageSize) + truncationNotice(meta,
|
|
169
|
+
function notices(meta, requestedPageSize, records4) {
|
|
170
|
+
return capNotice(meta, requestedPageSize) + truncationNotice(meta, records4);
|
|
1120
171
|
}
|
|
1121
172
|
function capNotice(meta, requestedPageSize) {
|
|
1122
173
|
const served = meta?.page_size;
|
|
@@ -1126,12 +177,12 @@ function capNotice(meta, requestedPageSize) {
|
|
|
1126
177
|
}
|
|
1127
178
|
return "";
|
|
1128
179
|
}
|
|
1129
|
-
function truncationNotice(meta,
|
|
180
|
+
function truncationNotice(meta, records4) {
|
|
1130
181
|
const total = meta?.total;
|
|
1131
182
|
const page = meta?.page;
|
|
1132
183
|
const pages = meta?.total_pages;
|
|
1133
184
|
if (typeof total !== "number" || typeof page !== "number" || typeof pages !== "number") return "";
|
|
1134
|
-
const shown =
|
|
185
|
+
const shown = records4.length;
|
|
1135
186
|
if (shown < total) {
|
|
1136
187
|
return `harness: showing ${shown} of ${total} (page ${page} of ${pages}) \u2014 pass --all for the rest, or --page to step through.
|
|
1137
188
|
`;
|
|
@@ -1142,20 +193,20 @@ async function fetchEveryPage(fetchPage) {
|
|
|
1142
193
|
const first = await fetchPage(1);
|
|
1143
194
|
const totalPages = pageCount(first.meta);
|
|
1144
195
|
if (totalPages === null) return first.data;
|
|
1145
|
-
let
|
|
196
|
+
let records4 = first.data;
|
|
1146
197
|
for (let page = 2; page <= totalPages; page++) {
|
|
1147
198
|
const body = await fetchPage(page);
|
|
1148
|
-
|
|
199
|
+
records4 = dedupe([...records4, ...body.data]);
|
|
1149
200
|
}
|
|
1150
|
-
return
|
|
201
|
+
return records4;
|
|
1151
202
|
}
|
|
1152
203
|
function pageCount(meta) {
|
|
1153
204
|
const total = meta?.total_pages;
|
|
1154
205
|
return typeof total === "number" && total > 0 ? total : null;
|
|
1155
206
|
}
|
|
1156
|
-
function dedupe(
|
|
207
|
+
function dedupe(records4) {
|
|
1157
208
|
const seen = /* @__PURE__ */ new Set();
|
|
1158
|
-
return
|
|
209
|
+
return records4.filter((record) => {
|
|
1159
210
|
const id = record["id"];
|
|
1160
211
|
const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
|
|
1161
212
|
if (seen.has(key)) return false;
|
|
@@ -1185,6 +236,9 @@ var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
|
|
|
1185
236
|
function isUuid(value) {
|
|
1186
237
|
return UUID_FORMAT.test(value);
|
|
1187
238
|
}
|
|
239
|
+
function isIdentifier(value) {
|
|
240
|
+
return IDENTIFIER_FORMAT.test(value);
|
|
241
|
+
}
|
|
1188
242
|
function requireIdentifiers(identifiers) {
|
|
1189
243
|
const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
|
|
1190
244
|
if (bad.length > 0) {
|
|
@@ -1194,9 +248,9 @@ function requireIdentifiers(identifiers) {
|
|
|
1194
248
|
async function ready(ctx, opts) {
|
|
1195
249
|
if (opts.claim === true) return claimNext(ctx, opts);
|
|
1196
250
|
const params = { ...listParams(ctx, opts), ready: true };
|
|
1197
|
-
const { records:
|
|
251
|
+
const { records: records4, notice } = await fetchIssues(ctx, params, opts);
|
|
1198
252
|
const rank = (issue) => PRIORITY_RANK[issue["priority"]] ?? 4;
|
|
1199
|
-
const sorted = markAgentSteps([...
|
|
253
|
+
const sorted = markAgentSteps([...records4].sort((a, b) => rank(a) - rank(b)));
|
|
1200
254
|
return { json: sorted, human: issuesTable(sorted, { markAgentSteps: true }), notice };
|
|
1201
255
|
}
|
|
1202
256
|
function markAgentSteps(issues) {
|
|
@@ -1240,7 +294,7 @@ async function claimNext(ctx, opts) {
|
|
|
1240
294
|
if (!isIssueRecord(claimed)) throw unreadable("carried neither an issue nor null");
|
|
1241
295
|
return {
|
|
1242
296
|
json: claimed,
|
|
1243
|
-
human: `Claimed ${
|
|
297
|
+
human: `Claimed ${str(claimed["identifier"])}: ${clean(claimed["title"])}
|
|
1244
298
|
`
|
|
1245
299
|
};
|
|
1246
300
|
}
|
|
@@ -1251,13 +305,13 @@ function isIssueRecord(value) {
|
|
|
1251
305
|
}
|
|
1252
306
|
async function blocked(ctx, opts) {
|
|
1253
307
|
const params = { ...listParams(ctx, opts), blocked: true };
|
|
1254
|
-
const { records:
|
|
1255
|
-
return { json:
|
|
308
|
+
const { records: records4, notice } = await fetchIssues(ctx, params, opts);
|
|
309
|
+
return { json: records4, human: issuesTable(records4), notice };
|
|
1256
310
|
}
|
|
1257
311
|
async function list(ctx, opts) {
|
|
1258
312
|
const params = listParams(ctx, opts);
|
|
1259
|
-
const { records:
|
|
1260
|
-
return { json:
|
|
313
|
+
const { records: records4, notice } = await fetchIssues(ctx, params, opts);
|
|
314
|
+
return { json: records4, human: issuesTable(records4), notice };
|
|
1261
315
|
}
|
|
1262
316
|
async function search(ctx, queryWords, opts) {
|
|
1263
317
|
if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
|
|
@@ -1281,11 +335,11 @@ async function show(ctx, identifier, opts) {
|
|
|
1281
335
|
if (opts.images !== false) {
|
|
1282
336
|
({ images, failures } = await fetchImages(ctx.client, issue, ctx.config.cacheDir));
|
|
1283
337
|
}
|
|
1284
|
-
const
|
|
338
|
+
const json = images.length === 0 ? issue : { ...issue, local_images: images };
|
|
1285
339
|
const human = issueDetail(issue, images);
|
|
1286
340
|
const notice = failures.map((failure) => `harness: image not downloaded: ${clean(failure)}
|
|
1287
341
|
`).join("");
|
|
1288
|
-
return notice === "" ? { json
|
|
342
|
+
return notice === "" ? { json, human } : { json, human, notice };
|
|
1289
343
|
}
|
|
1290
344
|
var ACTIVITY_MAX_LIMIT = 100;
|
|
1291
345
|
var ACTIVITY_INCLUDE = "condition_events,field_events";
|
|
@@ -1375,7 +429,7 @@ async function stats2(ctx) {
|
|
|
1375
429
|
async function prime(ctx) {
|
|
1376
430
|
const statsBody = await ctx.client.get("/api/v1/stats");
|
|
1377
431
|
const ready2 = markAgentSteps(await mostImportantReady(ctx));
|
|
1378
|
-
const
|
|
432
|
+
const json = { stats: statsBody.data, ready: ready2 };
|
|
1379
433
|
const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(ready2, { markAgentSteps: true }) + named(`
|
|
1380
434
|
Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
|
|
1381
435
|
harness close <id> --reason "Implemented in <commit-hash>" (response
|
|
@@ -1384,7 +438,7 @@ Create tracked work BEFORE coding: harness create --title "\u2026" --type task.
|
|
|
1384
438
|
Dependencies: harness dep add <issue> <blocker> (blocker blocks issue).
|
|
1385
439
|
Before reporting done: close the issues you finished.
|
|
1386
440
|
`);
|
|
1387
|
-
return { json
|
|
441
|
+
return { json, human };
|
|
1388
442
|
}
|
|
1389
443
|
async function mostImportantReady(ctx) {
|
|
1390
444
|
try {
|
|
@@ -1431,7 +485,7 @@ async function create(ctx, opts) {
|
|
|
1431
485
|
const issue = created.data;
|
|
1432
486
|
return {
|
|
1433
487
|
json: issue,
|
|
1434
|
-
human: `Created ${
|
|
488
|
+
human: `Created ${str(issue["identifier"])}: ${clean(issue["title"])}
|
|
1435
489
|
`
|
|
1436
490
|
};
|
|
1437
491
|
}
|
|
@@ -1484,7 +538,7 @@ async function update(ctx, identifier, opts) {
|
|
|
1484
538
|
if (opts.appendNotes !== void 0) {
|
|
1485
539
|
const current = await ctx.client.get(`/api/v1/issues/${identifier}`);
|
|
1486
540
|
const existing = current.data["notes"];
|
|
1487
|
-
const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${
|
|
541
|
+
const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${str(existing)}
|
|
1488
542
|
|
|
1489
543
|
${opts.appendNotes}`;
|
|
1490
544
|
patch["notes"] = combined;
|
|
@@ -1508,24 +562,48 @@ ${opts.appendNotes}`;
|
|
|
1508
562
|
if (issue === null) {
|
|
1509
563
|
throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
|
|
1510
564
|
}
|
|
1511
|
-
const human = wrote ? `Updated ${
|
|
1512
|
-
` : `Labels already up to date for ${
|
|
565
|
+
const human = wrote ? `Updated ${str(issue["identifier"])}
|
|
566
|
+
` : `Labels already up to date for ${str(issue["identifier"])}
|
|
1513
567
|
`;
|
|
1514
568
|
return { json: issue, human };
|
|
1515
569
|
}
|
|
1516
570
|
var CLAIM_REFUSAL_HINTS = {
|
|
1517
|
-
//
|
|
1518
|
-
// the step is
|
|
1519
|
-
|
|
571
|
+
// A person never claims an Operator step, so releasing the plan is not the
|
|
572
|
+
// fix: handing the step to a person is, and a handed-over Operator step
|
|
573
|
+
// stays frozen until the next release.
|
|
574
|
+
agent_step: (_identifier, plan) => {
|
|
575
|
+
const runs = plan.kind === "sole" ? `plan ${plan.root}` : plan.kind === "nearest" ? `its plans (several govern it; the nearest is ${plan.root})` : "its plan";
|
|
576
|
+
const target = plan.kind === "sole" ? plan.root : "<plan>";
|
|
577
|
+
return `a person can't claim an Operator step: Operator runs it for ${runs}. To do it yourself, set its executor to none (${invokedName()} plan edit ${target}) and release the plan again`;
|
|
578
|
+
},
|
|
579
|
+
plan_not_released: (_identifier, plan) => planReleaseHint(plan),
|
|
580
|
+
not_in_plan_revision: (_identifier, plan) => planReleaseHint(plan),
|
|
581
|
+
edited_since_release: (_identifier, plan) => planReleaseHint(plan),
|
|
1520
582
|
waiting_on_condition: (identifier) => `${invokedName()} show ${identifier} lists what it waits on; tick a gate with ${invokedName()} gate satisfy ${identifier} <gate>`
|
|
1521
583
|
};
|
|
584
|
+
function planReleaseHint(plan) {
|
|
585
|
+
if (plan.kind === "sole") return plan.release ? `run ${invokedName()} plan release ${plan.root}` : void 0;
|
|
586
|
+
if (plan.kind === "nearest") {
|
|
587
|
+
return `several plans govern it; the nearest is ${plan.root} (${invokedName()} plan show ${plan.root})`;
|
|
588
|
+
}
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
1522
591
|
function explainClaimRefusal(error, identifier) {
|
|
1523
592
|
if (!(error instanceof HttpError) || error.status !== 409) return error;
|
|
1524
593
|
const reason = error.body["reason"];
|
|
1525
|
-
const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier) : void 0;
|
|
594
|
+
const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier, refusingPlan(error.body)) : void 0;
|
|
1526
595
|
if (hint === void 0) return error;
|
|
1527
596
|
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
1528
597
|
}
|
|
598
|
+
function refusingPlan(body) {
|
|
599
|
+
const plan = body["plan"];
|
|
600
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return { kind: "none" };
|
|
601
|
+
const root = plan["root"];
|
|
602
|
+
if (typeof root !== "string" || !isIdentifier(root)) return { kind: "none" };
|
|
603
|
+
const message = typeof body["message"] === "string" ? body["message"] : "";
|
|
604
|
+
if (!new RegExp(`\\bplan ${root}\\b`).test(message)) return { kind: "nearest", root };
|
|
605
|
+
return { kind: "sole", root, release: new RegExp(`\\brelease plan ${root}\\b`).test(message) };
|
|
606
|
+
}
|
|
1529
607
|
function requireCloseArgs(identifiers, opts) {
|
|
1530
608
|
if (identifiers.length === 0) {
|
|
1531
609
|
throw new UsageError(
|
|
@@ -1562,7 +640,7 @@ async function close(ctx, identifiers, opts) {
|
|
|
1562
640
|
}
|
|
1563
641
|
}
|
|
1564
642
|
const human = results.map((result) => closeLine(result, opts.duplicateOf)).join("");
|
|
1565
|
-
const
|
|
643
|
+
const json = results.map(
|
|
1566
644
|
(result) => result.ok ? {
|
|
1567
645
|
identifier: result.identifier,
|
|
1568
646
|
closed: true,
|
|
@@ -1573,7 +651,7 @@ async function close(ctx, identifiers, opts) {
|
|
|
1573
651
|
} : { identifier: result.identifier, closed: false, error: errorText(result.error) }
|
|
1574
652
|
);
|
|
1575
653
|
const partial = results.some((result) => !result.ok);
|
|
1576
|
-
return partial ? { json
|
|
654
|
+
return partial ? { json, human, partial: true } : { json, human };
|
|
1577
655
|
}
|
|
1578
656
|
function closeLine(result, duplicateOf) {
|
|
1579
657
|
if (!result.ok) return `FAILED ${result.identifier}: ${errorText(result.error)}
|
|
@@ -1616,25 +694,25 @@ async function archive(ctx, identifiers, opts) {
|
|
|
1616
694
|
` : `FAILED ${result.identifier}: ${errorText(result.error)}
|
|
1617
695
|
`
|
|
1618
696
|
).join("");
|
|
1619
|
-
const
|
|
697
|
+
const json = results.map(
|
|
1620
698
|
(result) => result.ok ? { identifier: result.identifier, archived } : { identifier: result.identifier, error: errorText(result.error) }
|
|
1621
699
|
);
|
|
1622
700
|
const partial = results.some((result) => !result.ok);
|
|
1623
|
-
return partial ? { json
|
|
701
|
+
return partial ? { json, human, partial: true } : { json, human };
|
|
1624
702
|
}
|
|
1625
703
|
async function comments(ctx, identifier) {
|
|
1626
704
|
requireIdentifiers([identifier]);
|
|
1627
|
-
const
|
|
1628
|
-
if (
|
|
1629
|
-
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) => {
|
|
1630
708
|
const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
|
|
1631
709
|
const id = clean(comment2["id"] ?? "unknown id");
|
|
1632
|
-
const text = cleanMultiline(
|
|
1633
|
-
return `[${
|
|
710
|
+
const text = cleanMultiline(str(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
|
|
711
|
+
return `[${str(comment2["inserted_at"])}] ${author} (${id}):
|
|
1634
712
|
${text}
|
|
1635
713
|
`;
|
|
1636
714
|
}).join("\n");
|
|
1637
|
-
return { json:
|
|
715
|
+
return { json: records4, human };
|
|
1638
716
|
}
|
|
1639
717
|
async function completeCommentThread(ctx, identifier) {
|
|
1640
718
|
const pages = [];
|
|
@@ -1648,20 +726,20 @@ async function completeCommentThread(ctx, identifier) {
|
|
|
1648
726
|
if (!Array.isArray(page.data) || typeof page.meta?.has_more !== "boolean") {
|
|
1649
727
|
throw new CommandError("Invalid comment pagination: expected a comment array and boolean meta.has_more; completeness unknown.");
|
|
1650
728
|
}
|
|
1651
|
-
const
|
|
729
|
+
const records4 = [];
|
|
1652
730
|
for (const record of page.data) {
|
|
1653
731
|
if (record === null || typeof record !== "object" || Array.isArray(record) || typeof record.id !== "string" || record.id.trim() === "" || seen.has(record.id)) {
|
|
1654
732
|
throw new CommandError("Invalid comment pagination: missing or repeated comment id; completeness unknown.");
|
|
1655
733
|
}
|
|
1656
734
|
seen.add(record.id);
|
|
1657
|
-
|
|
735
|
+
records4.push(record);
|
|
1658
736
|
}
|
|
1659
|
-
pages.push(
|
|
737
|
+
pages.push(records4);
|
|
1660
738
|
if (!page.meta.has_more) return pages.reverse().flat();
|
|
1661
|
-
if (
|
|
739
|
+
if (records4.length === 0) {
|
|
1662
740
|
throw new CommandError("Invalid comment pagination: has_more on an empty page; completeness unknown.");
|
|
1663
741
|
}
|
|
1664
|
-
before =
|
|
742
|
+
before = records4[0]["id"];
|
|
1665
743
|
}
|
|
1666
744
|
}
|
|
1667
745
|
async function dep(ctx, verb, issue, blocker) {
|
|
@@ -1680,7 +758,7 @@ async function dep(ctx, verb, issue, blocker) {
|
|
|
1680
758
|
(candidate) => candidate["direction"] === "blocked_by" && candidate["other_issue"] === blocker
|
|
1681
759
|
);
|
|
1682
760
|
if (relation === void 0) throw new CommandError(`${blocker} does not block this issue.`);
|
|
1683
|
-
await ctx.client.delete(`/api/v1/issues/${issue}/relations/${
|
|
761
|
+
await ctx.client.delete(`/api/v1/issues/${issue}/relations/${str(relation["id"])}`);
|
|
1684
762
|
return { json: { deleted: relation["id"] }, human: `${blocker} no longer blocks ${issue}
|
|
1685
763
|
` };
|
|
1686
764
|
}
|
|
@@ -1803,17 +881,17 @@ function requireCommentId(commentId) {
|
|
|
1803
881
|
}
|
|
1804
882
|
var MAX_IMAGE_BYTES = 25e6;
|
|
1805
883
|
var MAX_VIDEO_BYTES = 1e8;
|
|
1806
|
-
async function attachImage(ctx,
|
|
1807
|
-
return uploadAttachment(ctx,
|
|
884
|
+
async function attachImage(ctx, path) {
|
|
885
|
+
return uploadAttachment(ctx, path, MAX_IMAGE_BYTES);
|
|
1808
886
|
}
|
|
1809
|
-
async function attachFile(ctx,
|
|
1810
|
-
const maxBytes = extname(
|
|
1811
|
-
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);
|
|
1812
890
|
}
|
|
1813
|
-
async function uploadAttachment(ctx,
|
|
1814
|
-
attachableFile(
|
|
1815
|
-
const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(
|
|
1816
|
-
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);
|
|
1817
895
|
const form = new FormData();
|
|
1818
896
|
form.set("file", file);
|
|
1819
897
|
const body = await ctx.client.postMultipart("/api/v1/uploads", form, file.size);
|
|
@@ -1822,27 +900,27 @@ async function uploadAttachment(ctx, path2, maxBytes) {
|
|
|
1822
900
|
if (typeof markdown !== "string") throw new CommandError("the upload response carried no markdown reference");
|
|
1823
901
|
return { json: attachment, human: clean(markdown) + "\n" };
|
|
1824
902
|
}
|
|
1825
|
-
function attachableFile(
|
|
903
|
+
function attachableFile(path, maxBytes) {
|
|
1826
904
|
let fileStats;
|
|
1827
905
|
try {
|
|
1828
|
-
fileStats = statSync2(
|
|
906
|
+
fileStats = statSync2(path);
|
|
1829
907
|
} catch (error) {
|
|
1830
|
-
throw new LocalError(`cannot read ${
|
|
908
|
+
throw new LocalError(`cannot read ${path}: ${posixDetail(error)}`);
|
|
1831
909
|
}
|
|
1832
910
|
if (!fileStats.isFile()) {
|
|
1833
911
|
const kind = fileStats.isDirectory() ? "directory" : "special file";
|
|
1834
|
-
throw new LocalError(`${
|
|
912
|
+
throw new LocalError(`${path} is a ${kind}, not a file.`);
|
|
1835
913
|
}
|
|
1836
|
-
requireAttachmentSize(
|
|
914
|
+
requireAttachmentSize(path, fileStats.size, maxBytes);
|
|
1837
915
|
try {
|
|
1838
|
-
closeSync(openSync(
|
|
916
|
+
closeSync(openSync(path, "r"));
|
|
1839
917
|
} catch (error) {
|
|
1840
|
-
throw new LocalError(`cannot read ${
|
|
918
|
+
throw new LocalError(`cannot read ${path}: ${posixDetail(error)}`);
|
|
1841
919
|
}
|
|
1842
920
|
}
|
|
1843
|
-
function requireAttachmentSize(
|
|
921
|
+
function requireAttachmentSize(path, size, maxBytes) {
|
|
1844
922
|
if (size > maxBytes) {
|
|
1845
|
-
throw new LocalError(`${
|
|
923
|
+
throw new LocalError(`${path} is too large: ${size} bytes, and attachments are capped at ${maxBytes}.`);
|
|
1846
924
|
}
|
|
1847
925
|
}
|
|
1848
926
|
function posixDetail(error) {
|
|
@@ -1851,19 +929,19 @@ function posixDetail(error) {
|
|
|
1851
929
|
if (code === "EACCES") return "permission denied";
|
|
1852
930
|
return error instanceof Error ? error.message : String(error);
|
|
1853
931
|
}
|
|
1854
|
-
async function setState(ctx, identifier,
|
|
932
|
+
async function setState(ctx, identifier, stateName) {
|
|
1855
933
|
const showBody = await ctx.client.get(`/api/v1/issues/${identifier}`);
|
|
1856
|
-
const teamKey =
|
|
934
|
+
const teamKey = str((showBody.data["team"] ?? {})["key"]);
|
|
1857
935
|
const statesBody = await ctx.client.get(
|
|
1858
936
|
`/api/v1/teams/${teamKey}/workflow_states`
|
|
1859
937
|
);
|
|
1860
|
-
const wanted =
|
|
938
|
+
const wanted = stateName.toLowerCase();
|
|
1861
939
|
const state = statesBody.data.find(
|
|
1862
|
-
(candidate) =>
|
|
940
|
+
(candidate) => str(candidate["name"]).toLowerCase() === wanted || str(candidate["type"]) === wanted
|
|
1863
941
|
);
|
|
1864
942
|
if (state === void 0) {
|
|
1865
|
-
const names = statesBody.data.map((candidate) =>
|
|
1866
|
-
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}`);
|
|
1867
945
|
}
|
|
1868
946
|
const body = await ctx.client.put(`/api/v1/issues/${identifier}/state`, { state_id: state["id"] });
|
|
1869
947
|
return body.data;
|
|
@@ -1894,16 +972,16 @@ function requestedLabelInputs(values) {
|
|
|
1894
972
|
});
|
|
1895
973
|
}
|
|
1896
974
|
function normalizeLabelName(value) {
|
|
1897
|
-
return
|
|
975
|
+
return str(value).trim().toLowerCase();
|
|
1898
976
|
}
|
|
1899
977
|
async function prepareCreateLabelIds(ctx, teamKey, names) {
|
|
1900
978
|
if (names.length === 0) return [];
|
|
1901
979
|
const teams = await ctx.client.get("/api/v1/teams");
|
|
1902
980
|
const target = teams.data.find(
|
|
1903
|
-
(team) =>
|
|
981
|
+
(team) => str(team["key"]).toLowerCase() === teamKey.toLowerCase()
|
|
1904
982
|
);
|
|
1905
983
|
if (target === void 0) throw new CommandError(`No team matches key "${teamKey}".`);
|
|
1906
|
-
const teamId =
|
|
984
|
+
const teamId = str(target["id"]);
|
|
1907
985
|
if (teamId === "") throw new CommandError(`team ${teamKey} carried no id`);
|
|
1908
986
|
return prepareApplicableLabelIds(ctx, teamId, names);
|
|
1909
987
|
}
|
|
@@ -1951,7 +1029,7 @@ async function resolveOrCreateApplicableLabel(ctx, catalog, name, teamId) {
|
|
|
1951
1029
|
}
|
|
1952
1030
|
}
|
|
1953
1031
|
function labelId(label) {
|
|
1954
|
-
const id =
|
|
1032
|
+
const id = str(label["id"]);
|
|
1955
1033
|
if (id === "") throw new CommandError("the label response carried no id");
|
|
1956
1034
|
return id;
|
|
1957
1035
|
}
|
|
@@ -1980,7 +1058,7 @@ async function prepareLabelUpdate(ctx, identifier, addInputs, removeInputs) {
|
|
|
1980
1058
|
const ids = originalIds.filter((id) => !removed.has(id));
|
|
1981
1059
|
if (addInputs.length > 0) {
|
|
1982
1060
|
const team = issue["team"] ?? {};
|
|
1983
|
-
const teamId =
|
|
1061
|
+
const teamId = str(team["id"]);
|
|
1984
1062
|
if (teamId === "") throw new CommandError(`issue ${identifier} carried no team id`);
|
|
1985
1063
|
let catalog = (await ctx.client.get("/api/v1/labels")).data;
|
|
1986
1064
|
for (const input of addInputs) {
|
|
@@ -2014,15 +1092,15 @@ async function fetchIssues(ctx, params, opts) {
|
|
|
2014
1092
|
throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
|
|
2015
1093
|
}
|
|
2016
1094
|
if (opts.all === true) {
|
|
2017
|
-
const
|
|
1095
|
+
const records5 = await fetchEveryPage(
|
|
2018
1096
|
(page) => ctx.client.get("/api/v1/issues", { ...params, page })
|
|
2019
1097
|
);
|
|
2020
|
-
return { records:
|
|
1098
|
+
return { records: records5, notice: "" };
|
|
2021
1099
|
}
|
|
2022
1100
|
const body = await ctx.client.get("/api/v1/issues", params);
|
|
2023
|
-
const
|
|
1101
|
+
const records4 = body.data;
|
|
2024
1102
|
const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
|
|
2025
|
-
return { records:
|
|
1103
|
+
return { records: records4, notice: notices(body.meta, requested, records4) };
|
|
2026
1104
|
}
|
|
2027
1105
|
function listParams(ctx, opts) {
|
|
2028
1106
|
if (opts.type !== void 0 && opts.label !== void 0) {
|
|
@@ -2115,7 +1193,7 @@ function errorText(error) {
|
|
|
2115
1193
|
if (error instanceof HttpError) return `${clean(error.message)} (HTTP ${error.status})`;
|
|
2116
1194
|
return `cannot reach the API: ${clean(error.message)}`;
|
|
2117
1195
|
}
|
|
2118
|
-
function
|
|
1196
|
+
function str(value) {
|
|
2119
1197
|
return value === null || value === void 0 ? "" : String(value);
|
|
2120
1198
|
}
|
|
2121
1199
|
|
|
@@ -2124,7 +1202,7 @@ var SAFE_ID = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
|
2124
1202
|
function requireSafeId(kind, id) {
|
|
2125
1203
|
if (!SAFE_ID.test(id)) throw new UsageError(`not a ${kind} id: ${id}`);
|
|
2126
1204
|
}
|
|
2127
|
-
function
|
|
1205
|
+
function str2(value) {
|
|
2128
1206
|
return value === null || value === void 0 ? "" : String(value);
|
|
2129
1207
|
}
|
|
2130
1208
|
function field(row, ...keys) {
|
|
@@ -2143,15 +1221,15 @@ function putDate(body, key, raw) {
|
|
|
2143
1221
|
if (parsed === null) return body;
|
|
2144
1222
|
return { ...body, [key]: parsed === "clear" ? null : parsed };
|
|
2145
1223
|
}
|
|
2146
|
-
async function paginatedRead(ctx,
|
|
1224
|
+
async function paginatedRead(ctx, path, params, opts) {
|
|
2147
1225
|
if (opts.all === true && opts.page !== void 0) {
|
|
2148
1226
|
throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
|
|
2149
1227
|
}
|
|
2150
1228
|
if (opts.all === true) {
|
|
2151
|
-
const
|
|
2152
|
-
return { records:
|
|
1229
|
+
const records4 = await fetchEveryPage((page) => ctx.client.get(path, { ...params, page }));
|
|
1230
|
+
return { records: records4, notice: "" };
|
|
2153
1231
|
}
|
|
2154
|
-
const body = await ctx.client.get(
|
|
1232
|
+
const body = await ctx.client.get(path, params);
|
|
2155
1233
|
const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
|
|
2156
1234
|
return { records: body.data, notice: notices(body.meta, requested, body.data) };
|
|
2157
1235
|
}
|
|
@@ -2159,18 +1237,18 @@ async function projectList(ctx, opts) {
|
|
|
2159
1237
|
let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
|
|
2160
1238
|
if (opts.search !== void 0) params = { ...params, search: opts.search };
|
|
2161
1239
|
if (opts.state !== void 0) params = { ...params, state: opts.state };
|
|
2162
|
-
const { records:
|
|
2163
|
-
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(
|
|
2164
1242
|
["ID", "STATE", "LEAD", "TARGET", "NAME"],
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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"])
|
|
2171
1249
|
])
|
|
2172
1250
|
);
|
|
2173
|
-
return { json:
|
|
1251
|
+
return { json: records4, human, notice };
|
|
2174
1252
|
}
|
|
2175
1253
|
async function projectShow(ctx, id) {
|
|
2176
1254
|
requireSafeId("project", id);
|
|
@@ -2191,12 +1269,12 @@ async function projectShow(ctx, id) {
|
|
|
2191
1269
|
];
|
|
2192
1270
|
const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
|
|
2193
1271
|
${title}:
|
|
2194
|
-
${
|
|
1272
|
+
${indent(String(text))}
|
|
2195
1273
|
`).join("");
|
|
2196
1274
|
const milestones = Array.isArray(project["milestones"]) ? project["milestones"] : [];
|
|
2197
|
-
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"])}
|
|
2198
1276
|
`).join("");
|
|
2199
|
-
const human = `${
|
|
1277
|
+
const human = `${str2(project["id"])} \xB7 ${clean(project["name"])}
|
|
2200
1278
|
` + fieldLines + sectionText + milestoneText;
|
|
2201
1279
|
return { json: project, human };
|
|
2202
1280
|
}
|
|
@@ -2215,7 +1293,7 @@ async function projectCreate(ctx, opts) {
|
|
|
2215
1293
|
if (opts.name === void 0) throw new UsageError("usage: harness project create --name <name> \u2026");
|
|
2216
1294
|
const body = await ctx.client.post("/api/v1/projects", projectBody(opts));
|
|
2217
1295
|
const project = body.data;
|
|
2218
|
-
return { json: project, human: `Created project ${clean(project["name"])} (${
|
|
1296
|
+
return { json: project, human: `Created project ${clean(project["name"])} (${str2(project["id"])})
|
|
2219
1297
|
` };
|
|
2220
1298
|
}
|
|
2221
1299
|
async function projectUpdate(ctx, id, opts) {
|
|
@@ -2225,7 +1303,7 @@ async function projectUpdate(ctx, id, opts) {
|
|
|
2225
1303
|
throw new UsageError("usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026");
|
|
2226
1304
|
}
|
|
2227
1305
|
const body = await ctx.client.patch(`/api/v1/projects/${id}`, patch);
|
|
2228
|
-
return { json: body.data, human: `Updated project ${
|
|
1306
|
+
return { json: body.data, human: `Updated project ${str2(body.data["id"])}
|
|
2229
1307
|
` };
|
|
2230
1308
|
}
|
|
2231
1309
|
async function milestoneList(ctx, project) {
|
|
@@ -2235,9 +1313,9 @@ async function milestoneList(ctx, project) {
|
|
|
2235
1313
|
const human = body.data.length === 0 ? "No milestones.\n" : table(
|
|
2236
1314
|
["ID", "TARGET", "NAME"],
|
|
2237
1315
|
body.data.map((milestone) => [
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
1316
|
+
str2(milestone["id"]),
|
|
1317
|
+
str2(milestone["target_date"]) || "-",
|
|
1318
|
+
str2(milestone["name"])
|
|
2241
1319
|
])
|
|
2242
1320
|
);
|
|
2243
1321
|
return { json: body.data, human };
|
|
@@ -2254,26 +1332,26 @@ async function milestoneCreate(ctx, opts) {
|
|
|
2254
1332
|
const milestone = response.data;
|
|
2255
1333
|
return {
|
|
2256
1334
|
json: milestone,
|
|
2257
|
-
human: `Created milestone ${clean(milestone["name"])} (${
|
|
1335
|
+
human: `Created milestone ${clean(milestone["name"])} (${str2(milestone["id"])})
|
|
2258
1336
|
`
|
|
2259
1337
|
};
|
|
2260
1338
|
}
|
|
2261
1339
|
async function docList(ctx, opts) {
|
|
2262
1340
|
let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
|
|
2263
1341
|
if (opts.search !== void 0) params = { ...params, search: opts.search };
|
|
2264
|
-
const { records:
|
|
2265
|
-
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(
|
|
2266
1344
|
["ID", "UPDATED", "TITLE"],
|
|
2267
|
-
|
|
1345
|
+
records4.map((doc) => [str2(doc["id"]), str2(doc["updated_at"]) || "-", str2(doc["title"])])
|
|
2268
1346
|
);
|
|
2269
|
-
return { json:
|
|
1347
|
+
return { json: records4, human, notice };
|
|
2270
1348
|
}
|
|
2271
1349
|
async function docShow(ctx, id) {
|
|
2272
1350
|
requireSafeId("document", id);
|
|
2273
1351
|
const body = await ctx.client.get(`/api/v1/documents/${id}`);
|
|
2274
1352
|
const doc = body.data;
|
|
2275
1353
|
const content = doc["content"];
|
|
2276
|
-
const human = `${
|
|
1354
|
+
const human = `${str2(doc["id"])} \xB7 ${clean(doc["title"])}
|
|
2277
1355
|
Updated: ${clean(doc["updated_at"])}
|
|
2278
1356
|
` + (content === null || content === void 0 || content === "" ? "" : "\n" + cleanMultiline(String(content)) + "\n");
|
|
2279
1357
|
return { json: doc, human };
|
|
@@ -2285,7 +1363,7 @@ async function docCreate(ctx, opts) {
|
|
|
2285
1363
|
body = putOpt2(body, "project_id", opts.project);
|
|
2286
1364
|
const response = await ctx.client.post("/api/v1/documents", body);
|
|
2287
1365
|
const doc = response.data;
|
|
2288
|
-
return { json: doc, human: `Created document ${clean(doc["title"])} (${
|
|
1366
|
+
return { json: doc, human: `Created document ${clean(doc["title"])} (${str2(doc["id"])})
|
|
2289
1367
|
` };
|
|
2290
1368
|
}
|
|
2291
1369
|
async function docUpdate(ctx, id, opts) {
|
|
@@ -2298,7 +1376,7 @@ async function docUpdate(ctx, id, opts) {
|
|
|
2298
1376
|
throw new UsageError("usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026");
|
|
2299
1377
|
}
|
|
2300
1378
|
const response = await ctx.client.patch(`/api/v1/documents/${id}`, patch);
|
|
2301
|
-
return { json: response.data, human: `Updated document ${
|
|
1379
|
+
return { json: response.data, human: `Updated document ${str2(response.data["id"])}
|
|
2302
1380
|
` };
|
|
2303
1381
|
}
|
|
2304
1382
|
async function linkList(ctx, issue) {
|
|
@@ -2306,7 +1384,7 @@ async function linkList(ctx, issue) {
|
|
|
2306
1384
|
const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
|
|
2307
1385
|
const human = body.data.length === 0 ? "No links.\n" : table(
|
|
2308
1386
|
["ID", "URL", "TITLE"],
|
|
2309
|
-
body.data.map((link) => [
|
|
1387
|
+
body.data.map((link) => [str2(link["id"]), str2(link["url"]), str2(link["title"]) || "-"])
|
|
2310
1388
|
);
|
|
2311
1389
|
return { json: body.data, human };
|
|
2312
1390
|
}
|
|
@@ -2339,7 +1417,7 @@ async function linkUpdate(ctx, issue, idOrUrl, opts) {
|
|
|
2339
1417
|
if (Object.keys(patch).length === 0) {
|
|
2340
1418
|
throw new UsageError("usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>");
|
|
2341
1419
|
}
|
|
2342
|
-
const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() :
|
|
1420
|
+
const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() : str2((await findLink(ctx, issue, idOrUrl))["id"]);
|
|
2343
1421
|
if (!isUuid(idOrUrl)) patch["expected_url"] = idOrUrl;
|
|
2344
1422
|
const response = await ctx.client.patch(`/api/v1/issues/${issue}/links/${id}`, patch);
|
|
2345
1423
|
return { json: response.data, human: `Updated link ${clean(response.data["url"])} on ${issue}
|
|
@@ -2348,19 +1426,19 @@ async function linkUpdate(ctx, issue, idOrUrl, opts) {
|
|
|
2348
1426
|
async function linkRm(ctx, issue, idOrUrl) {
|
|
2349
1427
|
requireIdentifiers([issue]);
|
|
2350
1428
|
const link = await findLink(ctx, issue, idOrUrl, true);
|
|
2351
|
-
await ctx.client.delete(`/api/v1/issues/${issue}/links/${
|
|
1429
|
+
await ctx.client.delete(`/api/v1/issues/${issue}/links/${str2(link["id"])}`);
|
|
2352
1430
|
return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
|
|
2353
1431
|
` };
|
|
2354
1432
|
}
|
|
2355
|
-
async function
|
|
1433
|
+
async function labelList(ctx) {
|
|
2356
1434
|
const body = await ctx.client.get("/api/v1/labels");
|
|
2357
1435
|
const human = body.data.length === 0 ? "No labels.\n" : table(
|
|
2358
1436
|
["NAME", "COLOR", "ISSUES", "ID"],
|
|
2359
1437
|
body.data.map((label) => [
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
1438
|
+
str2(label["name"]),
|
|
1439
|
+
str2(label["color"]) || "-",
|
|
1440
|
+
str2(label["issue_count"] ?? 0),
|
|
1441
|
+
str2(label["id"])
|
|
2364
1442
|
])
|
|
2365
1443
|
);
|
|
2366
1444
|
return { json: body.data, human };
|
|
@@ -2383,11 +1461,11 @@ async function teamCreate(ctx, opts) {
|
|
|
2383
1461
|
}
|
|
2384
1462
|
const created = await ctx.client.post("/api/v1/teams", { key: opts.key, name: opts.name });
|
|
2385
1463
|
const team = created.data;
|
|
2386
|
-
const states = await ctx.client.get(`/api/v1/teams/${
|
|
2387
|
-
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"])}
|
|
2388
1466
|
` + table(
|
|
2389
1467
|
["NAME", "TYPE"],
|
|
2390
|
-
states.data.map((state) => [
|
|
1468
|
+
states.data.map((state) => [str2(state["name"]), str2(state["type"])])
|
|
2391
1469
|
);
|
|
2392
1470
|
return { json: { ...team, workflow_states: states.data }, human };
|
|
2393
1471
|
}
|
|
@@ -2396,9 +1474,9 @@ async function teamList(ctx) {
|
|
|
2396
1474
|
const human = body.data.length === 0 ? "No teams.\n" : table(
|
|
2397
1475
|
["KEY", "NAME", "ISSUES", "DEFAULT"],
|
|
2398
1476
|
body.data.map((team) => [
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
1477
|
+
str2(team["key"]),
|
|
1478
|
+
str2(team["name"]),
|
|
1479
|
+
str2(team["issue_count"] ?? 0),
|
|
2402
1480
|
team["is_default"] === true ? "yes" : ""
|
|
2403
1481
|
])
|
|
2404
1482
|
);
|
|
@@ -2410,10 +1488,10 @@ async function workflowStates(ctx, team) {
|
|
|
2410
1488
|
const human = body.data.length === 0 ? "No workflow states.\n" : table(
|
|
2411
1489
|
["NAME", "TYPE", "COLOR", "ID"],
|
|
2412
1490
|
body.data.map((state) => [
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
1491
|
+
str2(state["name"]),
|
|
1492
|
+
str2(state["type"]),
|
|
1493
|
+
str2(state["color"]) || "-",
|
|
1494
|
+
str2(state["id"])
|
|
2417
1495
|
])
|
|
2418
1496
|
);
|
|
2419
1497
|
return { json: body.data, human };
|
|
@@ -2422,7 +1500,7 @@ async function userList(ctx) {
|
|
|
2422
1500
|
const body = await ctx.client.get("/api/v1/users");
|
|
2423
1501
|
const human = body.data.length === 0 ? "No users.\n" : table(
|
|
2424
1502
|
["NAME", "EMAIL", "ID"],
|
|
2425
|
-
body.data.map((user) => [
|
|
1503
|
+
body.data.map((user) => [str2(user["name"]), str2(user["email"]), str2(user["id"])])
|
|
2426
1504
|
);
|
|
2427
1505
|
return { json: body.data, human };
|
|
2428
1506
|
}
|
|
@@ -2431,7 +1509,7 @@ async function teamRename(ctx, key, name) {
|
|
|
2431
1509
|
throw new UsageError("usage: harness team rename <key> --name <name>");
|
|
2432
1510
|
}
|
|
2433
1511
|
const body = await ctx.client.patch(`/api/v1/teams/${encodeURIComponent(key)}`, { name });
|
|
2434
|
-
return { json: body.data, human: `Renamed ${
|
|
1512
|
+
return { json: body.data, human: `Renamed ${str2(body.data["key"])} to ${clean(body.data["name"])}
|
|
2435
1513
|
` };
|
|
2436
1514
|
}
|
|
2437
1515
|
async function teamSetDefault(ctx, key) {
|
|
@@ -2439,7 +1517,7 @@ async function teamSetDefault(ctx, key) {
|
|
|
2439
1517
|
const body = await ctx.client.put(`/api/v1/teams/${encodeURIComponent(key)}/default`, {});
|
|
2440
1518
|
return {
|
|
2441
1519
|
json: body.data,
|
|
2442
|
-
human: `${
|
|
1520
|
+
human: `${str2(body.data["key"])} is now the default filing target
|
|
2443
1521
|
`
|
|
2444
1522
|
};
|
|
2445
1523
|
}
|
|
@@ -2478,7 +1556,7 @@ async function stateOrder(ctx, ids, opts) {
|
|
|
2478
1556
|
const body = await ctx.client.put(`/api/v1/teams/${key}/workflow_states/order`, { state_ids });
|
|
2479
1557
|
const human = table(
|
|
2480
1558
|
["NAME", "TYPE"],
|
|
2481
|
-
body.data.map((state) => [
|
|
1559
|
+
body.data.map((state) => [str2(state["name"]), str2(state["type"])])
|
|
2482
1560
|
);
|
|
2483
1561
|
return { json: body.data, human };
|
|
2484
1562
|
}
|
|
@@ -2514,11 +1592,11 @@ async function memberList(ctx) {
|
|
|
2514
1592
|
const human = body.data.length === 0 ? "No members.\n" : table(
|
|
2515
1593
|
["NAME", "EMAIL", "RANK", "STANDING", "ID"],
|
|
2516
1594
|
body.data.map((member) => [
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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"))
|
|
2522
1600
|
])
|
|
2523
1601
|
);
|
|
2524
1602
|
return { json: body.data, human };
|
|
@@ -2530,7 +1608,7 @@ async function memberAdd(ctx, opts) {
|
|
|
2530
1608
|
const body = await ctx.client.post("/api/v1/members", { email: opts.email, role: opts.role });
|
|
2531
1609
|
return {
|
|
2532
1610
|
json: body.data,
|
|
2533
|
-
human: `Added ${
|
|
1611
|
+
human: `Added ${str2(field(body.data, "user", "email"))} as ${str2(body.data["role"])}
|
|
2534
1612
|
`
|
|
2535
1613
|
};
|
|
2536
1614
|
}
|
|
@@ -2539,19 +1617,19 @@ async function memberSetRole(ctx, userId, opts) {
|
|
|
2539
1617
|
throw new UsageError("usage: harness member set-role <user-id> --role <admin|staff|collaborator|none>");
|
|
2540
1618
|
}
|
|
2541
1619
|
const body = await ctx.client.put(`/api/v1/members/${encodeURIComponent(userId)}/role`, { role: opts.role });
|
|
2542
|
-
const rank =
|
|
2543
|
-
const human = rank === "" ? `Revoked the rank for ${
|
|
2544
|
-
` : `${
|
|
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}
|
|
2545
1623
|
`;
|
|
2546
1624
|
return { json: body.data, human };
|
|
2547
1625
|
}
|
|
2548
1626
|
function requireYes(command, id, yes) {
|
|
2549
1627
|
if (yes !== true) throw new UsageError(`${command} ${id} is destructive \u2014 pass --yes to confirm`);
|
|
2550
1628
|
}
|
|
2551
|
-
async function upload(ctx,
|
|
2552
|
-
return attachFile(ctx,
|
|
1629
|
+
async function upload(ctx, path) {
|
|
1630
|
+
return attachFile(ctx, path);
|
|
2553
1631
|
}
|
|
2554
|
-
function
|
|
1632
|
+
function indent(text) {
|
|
2555
1633
|
return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
|
|
2556
1634
|
}
|
|
2557
1635
|
|
|
@@ -2592,12 +1670,12 @@ async function toolsSearch(ctx, query, opts) {
|
|
|
2592
1670
|
}
|
|
2593
1671
|
async function toolsDomains(ctx) {
|
|
2594
1672
|
const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/capabilities/domains"));
|
|
2595
|
-
const domains =
|
|
1673
|
+
const domains = records(body.data, "domains");
|
|
2596
1674
|
return {
|
|
2597
1675
|
json: domains,
|
|
2598
1676
|
human: domains.length === 0 ? "Operator can use no capabilities here.\n" : table(
|
|
2599
1677
|
["DOMAIN", "CAPABILITIES"],
|
|
2600
|
-
domains.map((domain) => [
|
|
1678
|
+
domains.map((domain) => [str3(domain["domain"]), str3(domain["count"])])
|
|
2601
1679
|
)
|
|
2602
1680
|
};
|
|
2603
1681
|
}
|
|
@@ -2662,7 +1740,7 @@ async function toolShow(ctx, id, opts) {
|
|
|
2662
1740
|
}
|
|
2663
1741
|
async function skillsList(ctx) {
|
|
2664
1742
|
const body = await operatorRequest(() => ctx.client.get("/api/v1/operator/skills"));
|
|
2665
|
-
const skills =
|
|
1743
|
+
const skills = records(body.data, "skills").map((skill) => ({
|
|
2666
1744
|
id: skill["id"],
|
|
2667
1745
|
title: skill["title"],
|
|
2668
1746
|
invocation_name: skill["invocation_name"],
|
|
@@ -2673,10 +1751,10 @@ async function skillsList(ctx) {
|
|
|
2673
1751
|
human: skills.length === 0 ? "No skills are assigned to Operator in this organization.\n" : table(
|
|
2674
1752
|
["INVOCATION", "TITLE", "SHA-256", "ID"],
|
|
2675
1753
|
skills.map((skill) => [
|
|
2676
|
-
|
|
1754
|
+
str3(skill.invocation_name),
|
|
2677
1755
|
truncate(skill.title),
|
|
2678
|
-
|
|
2679
|
-
|
|
1756
|
+
str3(skill.content_sha256),
|
|
1757
|
+
str3(skill.id)
|
|
2680
1758
|
])
|
|
2681
1759
|
)
|
|
2682
1760
|
};
|
|
@@ -2736,7 +1814,7 @@ async function capabilityPage(ctx, params, staleCursor) {
|
|
|
2736
1814
|
void 0,
|
|
2737
1815
|
staleCursor
|
|
2738
1816
|
);
|
|
2739
|
-
const items =
|
|
1817
|
+
const items = records(body.data, "capabilities");
|
|
2740
1818
|
const meta = body.meta;
|
|
2741
1819
|
if (!isRecord(meta)) throw new CommandError("The capabilities response has no pagination metadata.");
|
|
2742
1820
|
const nextCursor = meta.next_cursor;
|
|
@@ -2771,14 +1849,14 @@ function explain(error, notFound2, staleCursor) {
|
|
|
2771
1849
|
function capabilitiesTable(items, search2) {
|
|
2772
1850
|
const headers = ["ID", "KIND", "DOMAIN", "SUMMARY"];
|
|
2773
1851
|
const rows = items.map((item) => [
|
|
2774
|
-
|
|
1852
|
+
str3(item["id"]),
|
|
2775
1853
|
kindName(item),
|
|
2776
|
-
|
|
1854
|
+
str3(item["domain"]),
|
|
2777
1855
|
truncate(item["summary"])
|
|
2778
1856
|
]);
|
|
2779
1857
|
const table2 = search2 ? table(
|
|
2780
1858
|
["SCORE", ...headers],
|
|
2781
|
-
rows.map((row, index) => [
|
|
1859
|
+
rows.map((row, index) => [str3(items[index]["score"]), ...row])
|
|
2782
1860
|
) : table(headers, rows);
|
|
2783
1861
|
return table2 + (items.some(needsApproval) ? "\n" + APPROVAL_NOTE : "");
|
|
2784
1862
|
}
|
|
@@ -2792,14 +1870,14 @@ function capabilityDetail(capability, view, schema) {
|
|
|
2792
1870
|
const kind = kindName(capability);
|
|
2793
1871
|
const line = (label, value) => ` ${(label + ":").padEnd(10)}${clean(value)}
|
|
2794
1872
|
`;
|
|
2795
|
-
const description = cleanMultiline(
|
|
1873
|
+
const description = cleanMultiline(str3(capability["description"]));
|
|
2796
1874
|
return `${clean(capability["id"])}
|
|
2797
|
-
` + 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 === "" ? "" : `
|
|
2798
1876
|
Description:
|
|
2799
|
-
${
|
|
1877
|
+
${indent2(description)}
|
|
2800
1878
|
`) + `
|
|
2801
1879
|
${view === "input" ? "Input" : "Result"} schema:
|
|
2802
|
-
${
|
|
1880
|
+
${indent2(cleanMultiline(JSON.stringify(schema, null, 2)))}
|
|
2803
1881
|
`;
|
|
2804
1882
|
}
|
|
2805
1883
|
function needsApproval(capability) {
|
|
@@ -2819,7 +1897,7 @@ var SCHEMA_PAGING_FIELDS = /* @__PURE__ */ new Set(["schema", "schema_encoding",
|
|
|
2819
1897
|
function withoutSchemaPaging(capability) {
|
|
2820
1898
|
return Object.fromEntries(Object.entries(capability).filter(([key]) => !SCHEMA_PAGING_FIELDS.has(key)));
|
|
2821
1899
|
}
|
|
2822
|
-
function
|
|
1900
|
+
function records(value, what) {
|
|
2823
1901
|
if (!Array.isArray(value) || !value.every(isRecord)) {
|
|
2824
1902
|
throw new CommandError(`The ${what} response carried no list.`);
|
|
2825
1903
|
}
|
|
@@ -2833,13 +1911,13 @@ function dedupeById(items) {
|
|
|
2833
1911
|
return true;
|
|
2834
1912
|
});
|
|
2835
1913
|
}
|
|
2836
|
-
function
|
|
1914
|
+
function indent2(text) {
|
|
2837
1915
|
return text.split("\n").map((line) => " " + line).join("\n");
|
|
2838
1916
|
}
|
|
2839
1917
|
function isRecord(value) {
|
|
2840
1918
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2841
1919
|
}
|
|
2842
|
-
function
|
|
1920
|
+
function str3(value) {
|
|
2843
1921
|
return value === null || value === void 0 ? "" : String(value);
|
|
2844
1922
|
}
|
|
2845
1923
|
|
|
@@ -3036,11 +2114,11 @@ async function jobChangeSet(ctx, id) {
|
|
|
3036
2114
|
if (!isRecord2(body.data)) throw new CommandError("The change set response carried no change set.");
|
|
3037
2115
|
return {
|
|
3038
2116
|
...pick(body.data, CHANGE_SET_FIELDS),
|
|
3039
|
-
actions:
|
|
2117
|
+
actions: records2(body.data["actions"]).map((action) => pick(action, CHANGE_SET_ACTION_FIELDS))
|
|
3040
2118
|
};
|
|
3041
2119
|
}
|
|
3042
2120
|
function jobPeople(job) {
|
|
3043
|
-
const participants =
|
|
2121
|
+
const participants = records2(field2(job, "people", "participants"));
|
|
3044
2122
|
const owner = isRecord2(job["owner"]) ? job["owner"] : null;
|
|
3045
2123
|
const names = /* @__PURE__ */ new Map();
|
|
3046
2124
|
for (const named2 of owner === null ? participants : [...participants, owner]) {
|
|
@@ -3111,7 +2189,7 @@ async function jobWatch(ctx, id, opts) {
|
|
|
3111
2189
|
after = page.nextAfter;
|
|
3112
2190
|
job = page.job;
|
|
3113
2191
|
if (page.hasMore) continue;
|
|
3114
|
-
const state =
|
|
2192
|
+
const state = str4(job["state"]);
|
|
3115
2193
|
const waitingOn = typeof job["waiting_on"] === "string" ? job["waiting_on"] : null;
|
|
3116
2194
|
if (shown === null || shown.state !== state || shown.waitingOn !== waitingOn) {
|
|
3117
2195
|
emit(stateLine(shown?.state ?? null, state, waitingOn));
|
|
@@ -3252,13 +2330,13 @@ async function explained(send, why) {
|
|
|
3252
2330
|
function jobsTable(jobs) {
|
|
3253
2331
|
if (jobs.length === 0) return "No Operator jobs.\n";
|
|
3254
2332
|
const rows = jobs.map((job) => [
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
2333
|
+
str4(job["id"]),
|
|
2334
|
+
str4(job["state"]) || "-",
|
|
2335
|
+
str4(job["waiting_on"]) || "-",
|
|
2336
|
+
str4(job["objective_kind"]) || "-",
|
|
3259
2337
|
personName(job["owner"]),
|
|
3260
|
-
|
|
3261
|
-
|
|
2338
|
+
str4(field2(job, "plan_step", "issue")) || "-",
|
|
2339
|
+
str4(job["updated_at"]) || "-"
|
|
3262
2340
|
]);
|
|
3263
2341
|
const table2 = table(["ID", "STATE", "WAITING ON", "KIND", "OWNER", "PLAN STEP", "UPDATED"], rows);
|
|
3264
2342
|
const approvals = jobs.filter((job) => typeof job["approval_url"] === "string");
|
|
@@ -3267,30 +2345,30 @@ function jobsTable(jobs) {
|
|
|
3267
2345
|
`).join("");
|
|
3268
2346
|
}
|
|
3269
2347
|
function jobDetail(job) {
|
|
3270
|
-
const waitingOn =
|
|
2348
|
+
const waitingOn = str4(job["waiting_on"]);
|
|
3271
2349
|
const header = `Operator job ${clean(job["id"])} \xB7 ${clean(job["state"])}` + (waitingOn === "" ? "" : ` \xB7 waiting on ${clean(waitingOn)}`) + "\n";
|
|
3272
|
-
const people =
|
|
2350
|
+
const people = records2(field2(job, "people", "participants"));
|
|
3273
2351
|
const fields = [
|
|
3274
|
-
["Kind",
|
|
3275
|
-
["Role",
|
|
2352
|
+
["Kind", str4(job["objective_kind"]) || "-"],
|
|
2353
|
+
["Role", str4(job["viewer_role"]) || null],
|
|
3276
2354
|
["Owner", isRecord2(job["owner"]) ? personName(job["owner"]) : null],
|
|
3277
2355
|
["Plan step", planStepLine(job["plan_step"])],
|
|
3278
2356
|
["Plan", planLine(job["plan"])],
|
|
3279
2357
|
["Progress", progressLine(job["progress"])],
|
|
3280
2358
|
["Window", windowLine(job["window"])],
|
|
3281
2359
|
["Spend", spendLine(job["spend"])],
|
|
3282
|
-
["Verification",
|
|
2360
|
+
["Verification", str4(field2(job, "verification", "status")) || null],
|
|
3283
2361
|
["Effects", effectsLine(job["effects"])],
|
|
3284
2362
|
["Skill", skillLine(job["skill"])],
|
|
3285
2363
|
["Grant", grantLine(job["grant"], people)],
|
|
3286
|
-
["Created",
|
|
3287
|
-
["Updated",
|
|
3288
|
-
["Workbench",
|
|
2364
|
+
["Created", str4(job["inserted_at"]) || null],
|
|
2365
|
+
["Updated", str4(job["updated_at"]) || null],
|
|
2366
|
+
["Workbench", str4(job["url"]) || null]
|
|
3289
2367
|
];
|
|
3290
|
-
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(
|
|
3291
2369
|
"Procedures",
|
|
3292
|
-
|
|
3293
|
-
(procedure) => `${
|
|
2370
|
+
records2(job["procedures"]),
|
|
2371
|
+
(procedure) => `${str4(procedure["name"])} v${str4(procedure["current_version"])} ${str4(procedure["id"])}`
|
|
3294
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";
|
|
3295
2373
|
}
|
|
3296
2374
|
function labelled(fields, width = 15) {
|
|
@@ -3299,16 +2377,16 @@ function labelled(fields, width = 15) {
|
|
|
3299
2377
|
}
|
|
3300
2378
|
function planStepLine(step) {
|
|
3301
2379
|
if (!isRecord2(step)) return null;
|
|
3302
|
-
return `${
|
|
2380
|
+
return `${str4(step["issue"])} (plan ${str4(step["plan_id"])})`;
|
|
3303
2381
|
}
|
|
3304
2382
|
function planLine(plan) {
|
|
3305
2383
|
if (!isRecord2(plan)) return "none yet";
|
|
3306
2384
|
const count = plan["capability_count"];
|
|
3307
|
-
return `version ${
|
|
2385
|
+
return `version ${str4(plan["version"])} \xB7 instructions v${str4(plan["instruction_version"])} \xB7 ${str4(count)} capabilit${count === 1 ? "y" : "ies"}`;
|
|
3308
2386
|
}
|
|
3309
2387
|
function progressLine(progress) {
|
|
3310
2388
|
if (!isRecord2(progress)) return null;
|
|
3311
|
-
const parts = [
|
|
2389
|
+
const parts = [str4(progress["stage"]), str4(progress["situation"])].filter((part) => part !== "");
|
|
3312
2390
|
if (progress["needs_viewer"] === true) parts.push("needs you");
|
|
3313
2391
|
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3314
2392
|
}
|
|
@@ -3317,28 +2395,28 @@ function windowLine(window) {
|
|
|
3317
2395
|
const left = duration(window["remaining_ms"]);
|
|
3318
2396
|
const budget = duration(window["budget_ms"]);
|
|
3319
2397
|
const sequence = window["sequence"];
|
|
3320
|
-
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})` : "");
|
|
3321
2399
|
}
|
|
3322
2400
|
function spendLine(spend) {
|
|
3323
2401
|
if (!isRecord2(spend)) return null;
|
|
3324
|
-
const spent = `${
|
|
2402
|
+
const spent = `${str4(spend["spent_cents"]) || "0"} cents spent`;
|
|
3325
2403
|
if (typeof spend["plan_budget_cents"] !== "number") return spent;
|
|
3326
2404
|
const remaining = spend["plan_remaining_cents"];
|
|
3327
2405
|
return `${spent} \xB7 plan budget ${spend["plan_budget_cents"]} cents` + (typeof remaining === "number" ? `, ${remaining} cents left` : "");
|
|
3328
2406
|
}
|
|
3329
2407
|
function effectsLine(effects) {
|
|
3330
2408
|
if (!isRecord2(effects)) return null;
|
|
3331
|
-
return ["staged", "succeeded", "failed", "unknown"].map((key) => `${
|
|
2409
|
+
return ["staged", "succeeded", "failed", "unknown"].map((key) => `${str4(effects[key]) || "0"} ${key}`).join(" \xB7 ");
|
|
3332
2410
|
}
|
|
3333
2411
|
function skillLine(skill) {
|
|
3334
2412
|
if (!isRecord2(skill)) return null;
|
|
3335
|
-
return `${
|
|
2413
|
+
return `${str4(skill["title"])} (${str4(skill["invocation_name"])}) \xB7 sha256 ${str4(skill["content_sha256"])} \xB7 pinned ${str4(skill["pinned_at"])}`;
|
|
3336
2414
|
}
|
|
3337
2415
|
function grantLine(grant, people) {
|
|
3338
2416
|
if (!isRecord2(grant)) return null;
|
|
3339
|
-
const approvedAt =
|
|
3340
|
-
const expiresAt =
|
|
3341
|
-
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}`}`;
|
|
3342
2420
|
}
|
|
3343
2421
|
function approvalSection(pending, people) {
|
|
3344
2422
|
if (!isRecord2(pending)) return "";
|
|
@@ -3347,19 +2425,19 @@ function approvalSection(pending, people) {
|
|
|
3347
2425
|
return "\nWaiting for approval. Approve it in the app; the CLI cannot:\n" + labelled([
|
|
3348
2426
|
[
|
|
3349
2427
|
"Change set",
|
|
3350
|
-
`${
|
|
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"])}`
|
|
3351
2429
|
],
|
|
3352
|
-
["Digest",
|
|
2430
|
+
["Digest", str4(pending["digest"]) || null],
|
|
3353
2431
|
["Approvers", approvers === "" ? canApprove : `${approvers} \xB7 ${canApprove}`],
|
|
3354
|
-
["Approve at",
|
|
2432
|
+
["Approve at", str4(pending["url"]) || null]
|
|
3355
2433
|
]);
|
|
3356
2434
|
}
|
|
3357
2435
|
function peopleSection(people) {
|
|
3358
2436
|
if (people.length === 0) return "";
|
|
3359
2437
|
const rows = people.map((person) => [
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
2438
|
+
str4(person["name"]) || "(no name)",
|
|
2439
|
+
str4(person["role"]),
|
|
2440
|
+
str4(person["user_id"])
|
|
3363
2441
|
]);
|
|
3364
2442
|
const lines = table(["", "", ""], rows).split("\n").slice(1, -1);
|
|
3365
2443
|
return "\nPeople:\n" + lines.map((line) => ` ${line}
|
|
@@ -3368,15 +2446,15 @@ function peopleSection(people) {
|
|
|
3368
2446
|
function changesText(id, job, changeSet) {
|
|
3369
2447
|
if (changeSet === null) return `No pending change set: Operator has proposed nothing for job ${id} yet.
|
|
3370
2448
|
`;
|
|
3371
|
-
const approvalUrl =
|
|
3372
|
-
const rows =
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
(Array.isArray(action["required_capability_ids"]) ? action["required_capability_ids"].map(
|
|
3378
|
-
|
|
3379
|
-
|
|
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"])
|
|
3380
2458
|
]);
|
|
3381
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}
|
|
3382
2460
|
`).join("");
|
|
@@ -3384,14 +2462,14 @@ function changesText(id, job, changeSet) {
|
|
|
3384
2462
|
` + labelled([
|
|
3385
2463
|
[
|
|
3386
2464
|
"Change set",
|
|
3387
|
-
`${
|
|
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`
|
|
3388
2466
|
],
|
|
3389
|
-
["Digest",
|
|
3390
|
-
approvalUrl === "" ? ["Workbench",
|
|
2467
|
+
["Digest", str4(changeSet["digest"]) || null],
|
|
2468
|
+
approvalUrl === "" ? ["Workbench", str4(job["url"]) || null] : ["Approve at", approvalUrl]
|
|
3391
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");
|
|
3392
2470
|
}
|
|
3393
2471
|
function peopleText(id, people) {
|
|
3394
|
-
const one = (entry) => isRecord2(entry) ?
|
|
2472
|
+
const one = (entry) => isRecord2(entry) ? str4(entry["name"]) || str4(entry["user_id"]) : "-";
|
|
3395
2473
|
const many = (entries) => (Array.isArray(entries) ? entries.map(one).join(", ") : "") || "none";
|
|
3396
2474
|
return `People on Operator job ${id}:
|
|
3397
2475
|
` + labelled(
|
|
@@ -3499,7 +2577,7 @@ function plural(count, noun) {
|
|
|
3499
2577
|
function uniqueById(jobs) {
|
|
3500
2578
|
const seen = /* @__PURE__ */ new Set();
|
|
3501
2579
|
return jobs.filter((job) => {
|
|
3502
|
-
const id =
|
|
2580
|
+
const id = str4(job["id"]);
|
|
3503
2581
|
if (seen.has(id)) return false;
|
|
3504
2582
|
seen.add(id);
|
|
3505
2583
|
return true;
|
|
@@ -3507,12 +2585,12 @@ function uniqueById(jobs) {
|
|
|
3507
2585
|
}
|
|
3508
2586
|
function personName(person) {
|
|
3509
2587
|
if (!isRecord2(person)) return "-";
|
|
3510
|
-
return
|
|
2588
|
+
return str4(person["name"]) || str4(person["user_id"]) || "-";
|
|
3511
2589
|
}
|
|
3512
2590
|
function nameOf(userId, people) {
|
|
3513
|
-
const id =
|
|
2591
|
+
const id = str4(userId);
|
|
3514
2592
|
const person = people.find((candidate) => candidate["user_id"] === id);
|
|
3515
|
-
return (person === void 0 ? "" :
|
|
2593
|
+
return (person === void 0 ? "" : str4(person["name"])) || id || "?";
|
|
3516
2594
|
}
|
|
3517
2595
|
function duration(ms) {
|
|
3518
2596
|
if (typeof ms !== "number" || !Number.isFinite(ms)) return null;
|
|
@@ -3538,13 +2616,13 @@ function field2(record, ...keys) {
|
|
|
3538
2616
|
}
|
|
3539
2617
|
return current ?? null;
|
|
3540
2618
|
}
|
|
3541
|
-
function
|
|
2619
|
+
function records2(value) {
|
|
3542
2620
|
return Array.isArray(value) ? value.filter(isRecord2) : [];
|
|
3543
2621
|
}
|
|
3544
2622
|
function isRecord2(value) {
|
|
3545
2623
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3546
2624
|
}
|
|
3547
|
-
function
|
|
2625
|
+
function str4(value) {
|
|
3548
2626
|
return value === null || value === void 0 ? "" : String(value);
|
|
3549
2627
|
}
|
|
3550
2628
|
|
|
@@ -3589,9 +2667,9 @@ function outcomeUnknown(why, write2) {
|
|
|
3589
2667
|
`Outcome unknown: ${why}, so ${write2.mayHave}. It is not retried; check ${write2.check} before trying again.`
|
|
3590
2668
|
);
|
|
3591
2669
|
}
|
|
3592
|
-
async function sendOnce(ctx,
|
|
2670
|
+
async function sendOnce(ctx, path, body, write2) {
|
|
3593
2671
|
try {
|
|
3594
|
-
return await ctx.client.post(
|
|
2672
|
+
return await ctx.client.post(path, body, { actor: false });
|
|
3595
2673
|
} catch (error) {
|
|
3596
2674
|
if (error instanceof TransportError) {
|
|
3597
2675
|
if (neverSent(error)) {
|
|
@@ -3730,14 +2808,14 @@ function startedText(job) {
|
|
|
3730
2808
|
async function run(ctx, request, opts = {}) {
|
|
3731
2809
|
const job = await startRun(ctx, request);
|
|
3732
2810
|
if (opts.watch !== true) return { json: job, human: startedText(job) };
|
|
3733
|
-
const
|
|
3734
|
-
if (!
|
|
2811
|
+
const json = opts.json === true;
|
|
2812
|
+
if (!json) opts.onLine?.(startedText(job) + "\n");
|
|
3735
2813
|
let watched;
|
|
3736
2814
|
try {
|
|
3737
2815
|
watched = await jobWatch(ctx, job.job_id, {
|
|
3738
2816
|
throughWaits: true,
|
|
3739
|
-
collectEvents:
|
|
3740
|
-
onLine:
|
|
2817
|
+
collectEvents: json,
|
|
2818
|
+
onLine: json ? void 0 : opts.onLine,
|
|
3741
2819
|
signal: opts.signal,
|
|
3742
2820
|
sleep: opts.sleep
|
|
3743
2821
|
});
|
|
@@ -3829,18 +2907,18 @@ function resolveTextSources(sources) {
|
|
|
3829
2907
|
}
|
|
3830
2908
|
return resolved;
|
|
3831
2909
|
}
|
|
3832
|
-
function readFileOption(flag,
|
|
3833
|
-
if (
|
|
2910
|
+
function readFileOption(flag, path) {
|
|
2911
|
+
if (path === STDIN_PATH) {
|
|
3834
2912
|
if (isatty(0)) {
|
|
3835
2913
|
throw new UsageError(`${flag} - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`);
|
|
3836
2914
|
}
|
|
3837
2915
|
} else {
|
|
3838
|
-
checkReadable(
|
|
2916
|
+
checkReadable(path, flag, DOCUMENT_LIMIT);
|
|
3839
2917
|
}
|
|
3840
|
-
return readTextFile(
|
|
2918
|
+
return readTextFile(path, flag, DOCUMENT_LIMIT);
|
|
3841
2919
|
}
|
|
3842
|
-
function readTextFile(
|
|
3843
|
-
if (
|
|
2920
|
+
function readTextFile(path, option, limit = TEXT_LIMIT) {
|
|
2921
|
+
if (path === STDIN_PATH) {
|
|
3844
2922
|
let piped;
|
|
3845
2923
|
try {
|
|
3846
2924
|
piped = readFileSync2(0);
|
|
@@ -3852,10 +2930,10 @@ function readTextFile(path2, option, limit = TEXT_LIMIT) {
|
|
|
3852
2930
|
}
|
|
3853
2931
|
return decodeUtf8(piped, `${option} -`);
|
|
3854
2932
|
}
|
|
3855
|
-
const flag = `${option} ${
|
|
2933
|
+
const flag = `${option} ${path}`;
|
|
3856
2934
|
let bytes;
|
|
3857
2935
|
try {
|
|
3858
|
-
bytes = readFileSync2(
|
|
2936
|
+
bytes = readFileSync2(path);
|
|
3859
2937
|
} catch (error) {
|
|
3860
2938
|
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
3861
2939
|
}
|
|
@@ -3872,11 +2950,11 @@ function decodeUtf8(bytes, flag) {
|
|
|
3872
2950
|
function oversize(what, limit) {
|
|
3873
2951
|
return `${what}: over ${limit.bytes / 1e6} MB \u2014 ${limit.why}`;
|
|
3874
2952
|
}
|
|
3875
|
-
function checkReadable(
|
|
3876
|
-
const flag = `${option} ${
|
|
2953
|
+
function checkReadable(path, option, limit = TEXT_LIMIT) {
|
|
2954
|
+
const flag = `${option} ${path}`;
|
|
3877
2955
|
let stats3;
|
|
3878
2956
|
try {
|
|
3879
|
-
stats3 = statSync3(
|
|
2957
|
+
stats3 = statSync3(path);
|
|
3880
2958
|
} catch (error) {
|
|
3881
2959
|
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
3882
2960
|
}
|
|
@@ -4076,7 +3154,7 @@ async function procedureSave(ctx, jobId, name) {
|
|
|
4076
3154
|
return {
|
|
4077
3155
|
json: pick2(data, ["id", "url"]),
|
|
4078
3156
|
human: `${saved}, but it can't be read back as you.
|
|
4079
|
-
` + labelled([["Workbench",
|
|
3157
|
+
` + labelled([["Workbench", str5(data["url"]) || null]])
|
|
4080
3158
|
};
|
|
4081
3159
|
}
|
|
4082
3160
|
const procedure = procedureJson(data);
|
|
@@ -4139,18 +3217,18 @@ async function startProcedureRun(ctx, request) {
|
|
|
4139
3217
|
return {
|
|
4140
3218
|
json: pick2(data, ["id", "url"]),
|
|
4141
3219
|
human: `${started}, but it can't be read back as you.
|
|
4142
|
-
` + labelled([["Workbench",
|
|
3220
|
+
` + labelled([["Workbench", str5(data["url"]) || null]]) + "\n" + RUN_NOTE
|
|
4143
3221
|
};
|
|
4144
3222
|
}
|
|
4145
3223
|
const job = withheld(data);
|
|
4146
|
-
const waitingOn =
|
|
3224
|
+
const waitingOn = str5(job["waiting_on"]);
|
|
4147
3225
|
return {
|
|
4148
3226
|
json: job,
|
|
4149
3227
|
human: `${started}.
|
|
4150
3228
|
` + labelled([
|
|
4151
|
-
["State",
|
|
4152
|
-
["Workbench",
|
|
4153
|
-
["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]
|
|
4154
3232
|
]) + "\n" + RUN_NOTE
|
|
4155
3233
|
};
|
|
4156
3234
|
}
|
|
@@ -4192,16 +3270,16 @@ function jsonValue(text, what) {
|
|
|
4192
3270
|
throw new UsageError(`${what}: not valid JSON after := (${firstLine(error)}); quote a string as name=value`);
|
|
4193
3271
|
}
|
|
4194
3272
|
}
|
|
4195
|
-
function readParamsFile(
|
|
4196
|
-
const text = readFileOption("--params-file",
|
|
3273
|
+
function readParamsFile(path) {
|
|
3274
|
+
const text = readFileOption("--params-file", path);
|
|
4197
3275
|
let parsed;
|
|
4198
3276
|
try {
|
|
4199
3277
|
parsed = JSON.parse(text);
|
|
4200
3278
|
} catch (error) {
|
|
4201
|
-
throw new UsageError(`--params-file ${
|
|
3279
|
+
throw new UsageError(`--params-file ${path}: not valid JSON (${firstLine(error)})`);
|
|
4202
3280
|
}
|
|
4203
3281
|
if (!isRecord4(parsed)) {
|
|
4204
|
-
throw new UsageError(`--params-file ${
|
|
3282
|
+
throw new UsageError(`--params-file ${path}: the parameters are one JSON object of names and values`);
|
|
4205
3283
|
}
|
|
4206
3284
|
return parsed;
|
|
4207
3285
|
}
|
|
@@ -4213,24 +3291,24 @@ function bareId(value, flag) {
|
|
|
4213
3291
|
function procedureJson(data) {
|
|
4214
3292
|
const procedure = pick2(data, PROCEDURE_FIELDS);
|
|
4215
3293
|
if ("versions" in procedure) {
|
|
4216
|
-
procedure["versions"] =
|
|
3294
|
+
procedure["versions"] = records3(procedure["versions"]).map((version2) => pick2(version2, PROCEDURE_VERSION_FIELDS));
|
|
4217
3295
|
}
|
|
4218
3296
|
return procedure;
|
|
4219
3297
|
}
|
|
4220
3298
|
function procedureDetail(procedure) {
|
|
4221
3299
|
const current = procedure["current_version"];
|
|
4222
|
-
const versions =
|
|
4223
|
-
(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"])}`)}
|
|
4224
3302
|
${"Capabilities:".padEnd(15)}${clean(list2(version2["capability_ids"]))}
|
|
4225
3303
|
${"Actions:".padEnd(15)}${clean(list2(version2["action_roster"]))}
|
|
4226
3304
|
`
|
|
4227
3305
|
);
|
|
4228
3306
|
return `Procedure ${clean(procedure["name"])} \xB7 v${clean(current)} \xB7 ${clean(procedure["id"])}
|
|
4229
3307
|
` + labelled([
|
|
4230
|
-
["Saved from", `job ${
|
|
4231
|
-
["Workbench",
|
|
4232
|
-
["Created",
|
|
4233
|
-
["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]
|
|
4234
3312
|
]) + (versions.length === 0 ? "\nVersions: none\n" : "\nVersions:\n" + versions.join("")) + parametersSection(procedure["parameter_schema"]) + "\n" + RUN_NOTE;
|
|
4235
3313
|
}
|
|
4236
3314
|
function parametersSection(schema) {
|
|
@@ -4238,7 +3316,7 @@ function parametersSection(schema) {
|
|
|
4238
3316
|
const lines = JSON.stringify(schema, null, 2).split("\n").map((line) => ` ${clean(line)}
|
|
4239
3317
|
`).join("");
|
|
4240
3318
|
const scoped = isRecord4(schema["scoped_inputs"]) ? schema["scoped_inputs"]["required"] : void 0;
|
|
4241
|
-
const scopes = Array.isArray(scoped) ? scoped.map(
|
|
3319
|
+
const scopes = Array.isArray(scoped) ? scoped.map(str5) : [];
|
|
4242
3320
|
let needs = "";
|
|
4243
3321
|
if (scopes.includes("contact_reconciliation")) {
|
|
4244
3322
|
needs = "Its runs also need --group <contact-group-id> and --source <uploaded-file-id> (contact_reconciliation).\n";
|
|
@@ -4249,18 +3327,18 @@ function parametersSection(schema) {
|
|
|
4249
3327
|
return "\nParameters a run takes (JSON schema):\n" + lines + needs;
|
|
4250
3328
|
}
|
|
4251
3329
|
function list2(value) {
|
|
4252
|
-
return (Array.isArray(value) ? value.map(
|
|
3330
|
+
return (Array.isArray(value) ? value.map(str5).join(", ") : "") || "none";
|
|
4253
3331
|
}
|
|
4254
3332
|
function pick2(record, keys) {
|
|
4255
3333
|
return Object.fromEntries(keys.filter((key) => key in record).map((key) => [key, record[key]]));
|
|
4256
3334
|
}
|
|
4257
|
-
function
|
|
3335
|
+
function records3(value) {
|
|
4258
3336
|
return Array.isArray(value) ? value.filter(isRecord4) : [];
|
|
4259
3337
|
}
|
|
4260
3338
|
function isRecord4(value) {
|
|
4261
3339
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4262
3340
|
}
|
|
4263
|
-
function
|
|
3341
|
+
function str5(value) {
|
|
4264
3342
|
return value === null || value === void 0 ? "" : String(value);
|
|
4265
3343
|
}
|
|
4266
3344
|
function firstLine(error) {
|
|
@@ -4273,6 +3351,9 @@ import { parseDocument } from "yaml";
|
|
|
4273
3351
|
var PLAN_EXECUTORS = ["none", "operator"];
|
|
4274
3352
|
var CONDITION_KINDS = ["not_before", "gate", "event"];
|
|
4275
3353
|
var GATE_OWNER_ROLES = ["gp", "compliance_officer", "valuer", "accountant", "auditor", "counsel"];
|
|
3354
|
+
var PLAN_STATUSES = ["held", "released", "running", "completed", "needs_attention"];
|
|
3355
|
+
var PLAN_LIST_MAX_LIMIT = 100;
|
|
3356
|
+
var PLAN_LIST_DEFAULT_LIMIT = 50;
|
|
4276
3357
|
function parsePlanDocument(text, source) {
|
|
4277
3358
|
const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
|
|
4278
3359
|
if (!isRecord5(parsed)) {
|
|
@@ -4311,8 +3392,8 @@ function jsonSafe(value) {
|
|
|
4311
3392
|
function firstLine2(error) {
|
|
4312
3393
|
return (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
|
|
4313
3394
|
}
|
|
4314
|
-
function readPlanFile(
|
|
4315
|
-
return parsePlanDocument(readFileOption(flag,
|
|
3395
|
+
function readPlanFile(path, flag = "--file") {
|
|
3396
|
+
return parsePlanDocument(readFileOption(flag, path), path);
|
|
4316
3397
|
}
|
|
4317
3398
|
async function planCreate(ctx, plan, opts) {
|
|
4318
3399
|
requireDocument(plan, "a plan is one object: the root's fields and its steps");
|
|
@@ -4325,7 +3406,7 @@ async function planCreate(ctx, plan, opts) {
|
|
|
4325
3406
|
throw explainMissing(error, `no team ${team} here, or this Harness server has no plan API yet`);
|
|
4326
3407
|
}
|
|
4327
3408
|
const created = body.data;
|
|
4328
|
-
const id =
|
|
3409
|
+
const id = str6(created["id"]);
|
|
4329
3410
|
return {
|
|
4330
3411
|
json: created,
|
|
4331
3412
|
human: `Created plan ${clean(id)} (${clean(created["status"]) || "held"})
|
|
@@ -4335,13 +3416,44 @@ Nothing runs until it is released: ${invokedName()} plan release ${clean(id)}
|
|
|
4335
3416
|
`
|
|
4336
3417
|
};
|
|
4337
3418
|
}
|
|
3419
|
+
function requirePlanListArgs(opts) {
|
|
3420
|
+
if (opts.status !== void 0 && !PLAN_STATUSES.includes(opts.status)) {
|
|
3421
|
+
throw new UsageError(`status must be one of: ${PLAN_STATUSES.join(", ")}`);
|
|
3422
|
+
}
|
|
3423
|
+
if (opts.limit !== void 0 && (!Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > PLAN_LIST_MAX_LIMIT)) {
|
|
3424
|
+
throw new UsageError(`--limit takes 1 to ${PLAN_LIST_MAX_LIMIT}: the server returns at most ${PLAN_LIST_MAX_LIMIT} plans`);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
async function planList(ctx, opts) {
|
|
3428
|
+
requirePlanListArgs(opts);
|
|
3429
|
+
const team = requiredTeam(ctx, opts.team);
|
|
3430
|
+
let body;
|
|
3431
|
+
try {
|
|
3432
|
+
body = await ctx.client.get(`/api/v1/teams/${team}/plans`, { status: opts.status, limit: opts.limit });
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
throw explainMissing(error, `no team ${team} here, or this Harness server cannot list plans yet`);
|
|
3435
|
+
}
|
|
3436
|
+
if (!Array.isArray(body.data)) throw new CommandError("The plan list response carried no list.");
|
|
3437
|
+
const list3 = body.data.filter(isRecord5);
|
|
3438
|
+
const limit = opts.limit ?? PLAN_LIST_DEFAULT_LIMIT;
|
|
3439
|
+
let notice;
|
|
3440
|
+
if (list3.length >= limit) {
|
|
3441
|
+
notice = limit < PLAN_LIST_MAX_LIMIT ? `harness: showing the newest ${list3.length} plans \u2014 pass --limit (up to ${PLAN_LIST_MAX_LIMIT}) for older ones.
|
|
3442
|
+
` : `harness: showing the newest ${list3.length} plans, the most one list returns \u2014 narrow it with --status.
|
|
3443
|
+
`;
|
|
3444
|
+
}
|
|
3445
|
+
const what = opts.status === void 0 ? "plans" : `${opts.status} plans`;
|
|
3446
|
+
const human = list3.length === 0 ? `No ${what} in team ${team}.
|
|
3447
|
+
` : plansTable(list3);
|
|
3448
|
+
return notice === void 0 ? { json: body.data, human } : { json: body.data, human, notice };
|
|
3449
|
+
}
|
|
4338
3450
|
async function planShow(ctx, id) {
|
|
4339
|
-
|
|
3451
|
+
requirePlanRef(id);
|
|
4340
3452
|
const plan = await planRequest(id, () => ctx.client.get(`/api/v1/plans/${id}`));
|
|
4341
3453
|
return { json: plan, human: planDetail(plan) };
|
|
4342
3454
|
}
|
|
4343
3455
|
async function planEdit(ctx, id, edits) {
|
|
4344
|
-
|
|
3456
|
+
requirePlanRef(id);
|
|
4345
3457
|
requireDocument(edits, "a plan edit is one object: steps to change and budgets");
|
|
4346
3458
|
const plan = await planRequest(id, () => ctx.client.patch(`/api/v1/plans/${id}`, edits));
|
|
4347
3459
|
const held = Array.isArray(plan["held_steps"]) && plan["held_steps"].length > 0;
|
|
@@ -4350,12 +3462,12 @@ async function planEdit(ctx, id, edits) {
|
|
|
4350
3462
|
human: `Updated plan ${clean(id)}
|
|
4351
3463
|
|
|
4352
3464
|
` + planDetail(plan) + (held ? `
|
|
4353
|
-
|
|
3465
|
+
Only Operator steps are held until the next release; release the plan again to take in the changes: ${invokedName()} plan release ${clean(id)}
|
|
4354
3466
|
` : "")
|
|
4355
3467
|
};
|
|
4356
3468
|
}
|
|
4357
3469
|
async function planRelease(ctx, id, opts) {
|
|
4358
|
-
|
|
3470
|
+
requirePlanRef(id);
|
|
4359
3471
|
const body = {};
|
|
4360
3472
|
if (opts.collaborators !== void 0 && opts.collaborators.length > 0) body["collaborators"] = opts.collaborators;
|
|
4361
3473
|
if (opts.retryBudget !== void 0) body["retry_budget"] = opts.retryBudget;
|
|
@@ -4370,7 +3482,7 @@ async function planRelease(ctx, id, opts) {
|
|
|
4370
3482
|
};
|
|
4371
3483
|
}
|
|
4372
3484
|
async function planHold(ctx, id) {
|
|
4373
|
-
|
|
3485
|
+
requirePlanRef(id);
|
|
4374
3486
|
const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/hold`));
|
|
4375
3487
|
return {
|
|
4376
3488
|
json: plan,
|
|
@@ -4383,25 +3495,41 @@ async function gateSet(ctx, issue, gate, satisfied) {
|
|
|
4383
3495
|
const wanted = gate.trim();
|
|
4384
3496
|
if (wanted === "") throw new UsageError("name the gate by its key, its label or its condition id");
|
|
4385
3497
|
const expert = isExpertToken(ctx.config.token);
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
3498
|
+
let list3 = null;
|
|
3499
|
+
if (!isUuid(wanted)) {
|
|
3500
|
+
try {
|
|
3501
|
+
list3 = await readConditions(ctx, issue, expert);
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
if (error instanceof HttpError && error.status === 403) {
|
|
3504
|
+
throw new HttpError(
|
|
3505
|
+
error.status,
|
|
3506
|
+
`${error.message} \u2014 find the gate by its condition id; only Harness staff can list an issue's conditions`,
|
|
3507
|
+
error.body,
|
|
3508
|
+
error.retryAfter
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
throw error;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
const target = list3 === null ? { id: wanted.toLowerCase(), label: wanted.toLowerCase() } : findGate(list3, issue, wanted, expert);
|
|
3515
|
+
const id = str6(target["id"]);
|
|
4389
3516
|
let body;
|
|
4390
3517
|
try {
|
|
4391
3518
|
body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
|
|
4392
3519
|
} catch (error) {
|
|
4393
|
-
throw await explainNotGateOwner(ctx, error, target, issue, satisfied);
|
|
3520
|
+
throw await explainNotGateOwner(ctx, error, target, issue, satisfied, list3 !== null);
|
|
4394
3521
|
}
|
|
4395
|
-
const others = list3
|
|
3522
|
+
const others = list3?.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length ?? 0;
|
|
4396
3523
|
let tail = "";
|
|
4397
|
-
if (satisfied && expert) {
|
|
3524
|
+
if (satisfied && list3 !== null && expert) {
|
|
4398
3525
|
tail = others > 0 ? ` \u2014 ${others} other gate${others === 1 ? "" : "s"} of yours on it ${others === 1 ? "is" : "are"} open` : "";
|
|
4399
|
-
} else if (satisfied) {
|
|
3526
|
+
} else if (satisfied && list3 !== null) {
|
|
4400
3527
|
tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
|
|
4401
3528
|
}
|
|
3529
|
+
const label = list3 === null && isRecord5(body.data) ? body.data["label"] ?? id : target["label"];
|
|
4402
3530
|
return {
|
|
4403
3531
|
json: body.data,
|
|
4404
|
-
human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(
|
|
3532
|
+
human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(label)}" on ${issue}${tail}
|
|
4405
3533
|
`
|
|
4406
3534
|
};
|
|
4407
3535
|
}
|
|
@@ -4435,8 +3563,10 @@ async function readConditions(ctx, issue, expert) {
|
|
|
4435
3563
|
if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
|
|
4436
3564
|
return conditions.filter(isRecord5);
|
|
4437
3565
|
}
|
|
4438
|
-
function
|
|
4439
|
-
if (!isUuid(id)
|
|
3566
|
+
function requirePlanRef(id) {
|
|
3567
|
+
if (!isUuid(id) && !isIdentifier(id)) {
|
|
3568
|
+
throw new UsageError(`not a plan: ${id} (expected the plan's id or its root issue's identifier, e.g. FR-1)`);
|
|
3569
|
+
}
|
|
4440
3570
|
}
|
|
4441
3571
|
function requireDocument(value, what) {
|
|
4442
3572
|
if (!isRecord5(value)) throw new UsageError(what);
|
|
@@ -4445,18 +3575,21 @@ async function planRequest(id, send) {
|
|
|
4445
3575
|
try {
|
|
4446
3576
|
return (await send()).data;
|
|
4447
3577
|
} catch (error) {
|
|
4448
|
-
throw explainMissing(
|
|
3578
|
+
throw explainMissing(
|
|
3579
|
+
error,
|
|
3580
|
+
isUuid(id) ? `no plan ${id} in this organization, or this Harness server has no plan API yet` : `${id} roots no plan in this organization (${invokedName()} show ${id} names the plan an issue belongs to, by its root), or this Harness server opens plans by their id only`
|
|
3581
|
+
);
|
|
4449
3582
|
}
|
|
4450
3583
|
}
|
|
4451
3584
|
function explainMissing(error, why) {
|
|
4452
3585
|
if (!(error instanceof HttpError) || error.status !== 404) return error;
|
|
4453
3586
|
return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
|
|
4454
3587
|
}
|
|
4455
|
-
async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
3588
|
+
async function explainNotGateOwner(ctx, error, gate, issue, satisfied, allowLookup) {
|
|
4456
3589
|
if (!(error instanceof HttpError) || error.status !== 403 || error.body["reason"] !== "not_gate_owner") return error;
|
|
4457
3590
|
let current;
|
|
4458
3591
|
let currentOwner = error.body["owner"];
|
|
4459
|
-
if (currentOwner !== null && !isRecord5(currentOwner)) {
|
|
3592
|
+
if (allowLookup && currentOwner !== null && !isRecord5(currentOwner)) {
|
|
4460
3593
|
try {
|
|
4461
3594
|
const fresh = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
|
|
4462
3595
|
if (Array.isArray(fresh)) current = fresh.filter(isRecord5).find((condition) => condition["id"] === gate["id"]);
|
|
@@ -4470,26 +3603,20 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
|
4470
3603
|
const message = satisfied ? `Cannot satisfy ${named2}: only ${subject} can tick it` : `Cannot un-satisfy ${named2}: only ${subject} or Harness staff can untick it`;
|
|
4471
3604
|
return new HttpError(error.status, message, error.body, error.retryAfter);
|
|
4472
3605
|
}
|
|
4473
|
-
var gateLabel = (condition) =>
|
|
4474
|
-
var gateKey = (condition) =>
|
|
3606
|
+
var gateLabel = (condition) => str6(condition["label"]).trim();
|
|
3607
|
+
var gateKey = (condition) => str6(condition["key"]);
|
|
4475
3608
|
function exactMatches(conditions, wanted) {
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
if (condition["kind"] !== "gate") return false;
|
|
4480
|
-
const key = gateKey(condition);
|
|
4481
|
-
return key !== "" && key === (id ?? wanted) || gateLabel(condition) === wanted;
|
|
4482
|
-
});
|
|
3609
|
+
return conditions.filter(
|
|
3610
|
+
(condition) => condition["kind"] === "gate" && (gateKey(condition) !== "" && gateKey(condition) === wanted || gateLabel(condition) === wanted)
|
|
3611
|
+
);
|
|
4483
3612
|
}
|
|
4484
3613
|
function ambiguous(conditions, issue, wanted, matches) {
|
|
4485
3614
|
const listed = matches.map((condition) => {
|
|
4486
3615
|
const what = condition["kind"] === "gate" ? `gate ${gateName({ label: gateLabel(condition), key: gateKey(condition) })}` : `${clean(condition["kind"])} condition`;
|
|
4487
3616
|
return `${clean(condition["id"])} (${what})`;
|
|
4488
3617
|
}).join(", ");
|
|
4489
|
-
const unnameable = matches.map((condition) => str7(condition["id"])).filter((id) => exactMatches(conditions, id).length !== 1).map((id) => clean(id));
|
|
4490
|
-
const caveat = unnameable.length === 0 ? "" : ` (${unnameable.join(", ")} ${unnameable.length === 1 ? "is" : "are"} also another gate's key or label, so ${unnameable.length === 1 ? "it" : "they"} cannot be named by id)`;
|
|
4491
3618
|
return new CommandError(
|
|
4492
|
-
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean
|
|
3619
|
+
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean`
|
|
4493
3620
|
);
|
|
4494
3621
|
}
|
|
4495
3622
|
function findGate(conditions, issue, wanted, expert = false) {
|
|
@@ -4497,11 +3624,6 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4497
3624
|
if (exact.length > 1) throw ambiguous(conditions, issue, wanted, exact);
|
|
4498
3625
|
if (exact.length === 1) {
|
|
4499
3626
|
const only = exact[0];
|
|
4500
|
-
if (only["kind"] !== "gate") {
|
|
4501
|
-
throw new CommandError(
|
|
4502
|
-
`condition ${wanted} on ${issue} is a ${clean(only["kind"])} condition \u2014 only a gate is satisfied by hand`
|
|
4503
|
-
);
|
|
4504
|
-
}
|
|
4505
3627
|
return only;
|
|
4506
3628
|
}
|
|
4507
3629
|
const gates = conditions.filter((condition) => condition["kind"] === "gate");
|
|
@@ -4511,7 +3633,6 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4511
3633
|
);
|
|
4512
3634
|
if (loose.length > 1) throw ambiguous(conditions, issue, wanted, loose);
|
|
4513
3635
|
if (loose.length === 1) return loose[0];
|
|
4514
|
-
if (isUuid(wanted)) throw new CommandError(`No condition ${wanted} on ${issue}${expert ? " that you own" : ""}.`);
|
|
4515
3636
|
const none = expert ? `No gate of yours on ${issue}` : `No gate on ${issue}`;
|
|
4516
3637
|
if (gates.length === 0) {
|
|
4517
3638
|
throw new CommandError(`${none} matches "${wanted}" \u2014 ${expert ? "you own no gate on it" : "it has no gates"}`);
|
|
@@ -4522,7 +3643,7 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4522
3643
|
function isRecord5(value) {
|
|
4523
3644
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4524
3645
|
}
|
|
4525
|
-
function
|
|
3646
|
+
function str6(value) {
|
|
4526
3647
|
return value === null || value === void 0 ? "" : String(value);
|
|
4527
3648
|
}
|
|
4528
3649
|
|
|
@@ -4543,265 +3664,7 @@ function isRecord6(value) {
|
|
|
4543
3664
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4544
3665
|
}
|
|
4545
3666
|
|
|
4546
|
-
// src/core/credentials.ts
|
|
4547
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4548
|
-
import { homedir } from "os";
|
|
4549
|
-
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
4550
|
-
function defaultCredentialsPath(env = process.env) {
|
|
4551
|
-
const xdg = env["XDG_CONFIG_HOME"];
|
|
4552
|
-
const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
|
|
4553
|
-
return join2(base, "harness", "credentials.json");
|
|
4554
|
-
}
|
|
4555
|
-
function readCredentials(path2) {
|
|
4556
|
-
try {
|
|
4557
|
-
const parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
4558
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
4559
|
-
const { url, token } = parsed;
|
|
4560
|
-
if (typeof url === "string" && typeof token === "string") return { url, token };
|
|
4561
|
-
}
|
|
4562
|
-
} catch {
|
|
4563
|
-
}
|
|
4564
|
-
return null;
|
|
4565
|
-
}
|
|
4566
|
-
var uniqueCounter = 0;
|
|
4567
|
-
function writeCredentials(path2, url, token) {
|
|
4568
|
-
mkdirSync2(dirname2(path2), { recursive: true });
|
|
4569
|
-
const tempPath = `${path2}.${process.pid}.${++uniqueCounter}.tmp`;
|
|
4570
|
-
try {
|
|
4571
|
-
writeFileSync2(tempPath, "", { mode: 384 });
|
|
4572
|
-
chmodSync2(tempPath, 384);
|
|
4573
|
-
writeFileSync2(tempPath, JSON.stringify({ url, token }));
|
|
4574
|
-
renameSync2(tempPath, path2);
|
|
4575
|
-
} finally {
|
|
4576
|
-
rmSync2(tempPath, { force: true });
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
function deleteCredentials(path2) {
|
|
4580
|
-
rmSync2(path2, { force: true });
|
|
4581
|
-
}
|
|
4582
|
-
|
|
4583
|
-
// src/core/repo-config.ts
|
|
4584
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4585
|
-
import { homedir as homedir2 } from "os";
|
|
4586
|
-
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
|
|
4587
|
-
var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
|
|
4588
|
-
function findRepoConfig(startDir = process.cwd()) {
|
|
4589
|
-
let dir = startDir;
|
|
4590
|
-
const { root } = parse(startDir);
|
|
4591
|
-
for (; ; ) {
|
|
4592
|
-
const candidate = join3(dir, CONFIG_RELATIVE_PATH);
|
|
4593
|
-
if (existsSync(candidate)) {
|
|
4594
|
-
return readRepoConfig(candidate);
|
|
4595
|
-
}
|
|
4596
|
-
if (dir === root) return { config: null, warnings: [] };
|
|
4597
|
-
dir = dirname3(dir);
|
|
4598
|
-
}
|
|
4599
|
-
}
|
|
4600
|
-
function readRepoConfig(path2) {
|
|
4601
|
-
let parsed;
|
|
4602
|
-
try {
|
|
4603
|
-
parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
4604
|
-
} catch (error) {
|
|
4605
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
4606
|
-
return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
|
|
4607
|
-
}
|
|
4608
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4609
|
-
return { config: null, warnings: [`harness: ignoring ${path2} \u2014 expected a JSON object`] };
|
|
4610
|
-
}
|
|
4611
|
-
const record = parsed;
|
|
4612
|
-
const config = { path: path2 };
|
|
4613
|
-
const warnings = [];
|
|
4614
|
-
for (const field3 of ["team", "url", "org"]) {
|
|
4615
|
-
const value = record[field3];
|
|
4616
|
-
if (value === void 0) continue;
|
|
4617
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
4618
|
-
config[field3] = value.trim();
|
|
4619
|
-
} else {
|
|
4620
|
-
warnings.push(`harness: ignoring "${field3}" in ${path2} \u2014 expected a non-empty string`);
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
return { config, warnings };
|
|
4624
|
-
}
|
|
4625
|
-
function trustedHostsPath(env = process.env) {
|
|
4626
|
-
const xdg = env["XDG_CONFIG_HOME"];
|
|
4627
|
-
const base = xdg !== void 0 && xdg !== "" && isAbsolute2(xdg) ? xdg : join3(homedir2(), ".config");
|
|
4628
|
-
return join3(base, "harness", "trusted-hosts.json");
|
|
4629
|
-
}
|
|
4630
|
-
function readTrustedHosts(env) {
|
|
4631
|
-
try {
|
|
4632
|
-
const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
|
|
4633
|
-
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
|
|
4634
|
-
return parsed["hosts"].filter(
|
|
4635
|
-
(host) => typeof host === "string"
|
|
4636
|
-
);
|
|
4637
|
-
}
|
|
4638
|
-
} catch {
|
|
4639
|
-
}
|
|
4640
|
-
return [];
|
|
4641
|
-
}
|
|
4642
|
-
function isHostTrusted(baseUrl, env) {
|
|
4643
|
-
return readTrustedHosts(env).includes(baseUrl);
|
|
4644
|
-
}
|
|
4645
|
-
function trustHost(baseUrl, env) {
|
|
4646
|
-
const hosts = readTrustedHosts(env);
|
|
4647
|
-
if (hosts.includes(baseUrl)) return;
|
|
4648
|
-
const path2 = trustedHostsPath(env);
|
|
4649
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
4650
|
-
writeFileSync3(path2, JSON.stringify({ hosts: [...hosts, baseUrl] }, null, 2) + "\n");
|
|
4651
|
-
}
|
|
4652
|
-
|
|
4653
|
-
// src/core/config.ts
|
|
4654
|
-
import { homedir as homedir3, tmpdir } from "os";
|
|
4655
|
-
import { isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
4656
|
-
var DEFAULT_BASE_URL = "https://harness.infra.one";
|
|
4657
|
-
function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
4658
|
-
let warnings = [];
|
|
4659
|
-
if (repo === void 0) {
|
|
4660
|
-
const lookup = findRepoConfig();
|
|
4661
|
-
repo = lookup.config;
|
|
4662
|
-
warnings = lookup.warnings;
|
|
4663
|
-
}
|
|
4664
|
-
const path2 = credentialsPath ?? defaultCredentialsPath(env);
|
|
4665
|
-
const stored = readCredentials(path2);
|
|
4666
|
-
const envUrl = blankToNull(env["HARNESS_API_URL"]);
|
|
4667
|
-
const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
|
|
4668
|
-
const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
|
|
4669
|
-
let rawUrl;
|
|
4670
|
-
let hostSource;
|
|
4671
|
-
if (envUrl !== null) {
|
|
4672
|
-
rawUrl = envUrl;
|
|
4673
|
-
hostSource = "env";
|
|
4674
|
-
} else if (envToken !== null) {
|
|
4675
|
-
if (repoUrl !== null && normalize(repoUrl) !== normalize(DEFAULT_BASE_URL)) {
|
|
4676
|
-
return {
|
|
4677
|
-
kind: "error",
|
|
4678
|
-
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`,
|
|
4679
|
-
warnings
|
|
4680
|
-
};
|
|
4681
|
-
}
|
|
4682
|
-
rawUrl = DEFAULT_BASE_URL;
|
|
4683
|
-
hostSource = "default";
|
|
4684
|
-
} else if (repoUrl !== null) {
|
|
4685
|
-
rawUrl = repoUrl;
|
|
4686
|
-
hostSource = "repo-config";
|
|
4687
|
-
} else if (stored !== null) {
|
|
4688
|
-
rawUrl = stored.url;
|
|
4689
|
-
hostSource = "stored-login";
|
|
4690
|
-
} else {
|
|
4691
|
-
rawUrl = DEFAULT_BASE_URL;
|
|
4692
|
-
hostSource = "default";
|
|
4693
|
-
}
|
|
4694
|
-
const validated = validateBaseUrl(rawUrl);
|
|
4695
|
-
if (!validated.ok) return { kind: "error", message: validated.message, warnings };
|
|
4696
|
-
const baseUrl = rawUrl.replace(/\/+$/, "");
|
|
4697
|
-
let token;
|
|
4698
|
-
if (envToken !== null) {
|
|
4699
|
-
token = envToken;
|
|
4700
|
-
} else if (stored !== null && normalize(stored.url) !== null && normalize(stored.url) === normalize(baseUrl)) {
|
|
4701
|
-
token = stored.token;
|
|
4702
|
-
} else {
|
|
4703
|
-
const result = { kind: "needs-login", baseUrl, hostSource, warnings };
|
|
4704
|
-
if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
|
|
4705
|
-
return result;
|
|
4706
|
-
}
|
|
4707
|
-
return {
|
|
4708
|
-
kind: "ok",
|
|
4709
|
-
config: {
|
|
4710
|
-
baseUrl,
|
|
4711
|
-
token,
|
|
4712
|
-
actor: blankToNull(env["HARNESS_ACTOR"]),
|
|
4713
|
-
// The committed team is the fallback: the environment (and, above it,
|
|
4714
|
-
// the --team flag at the command layer) stays authoritative.
|
|
4715
|
-
team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
|
|
4716
|
-
cacheDir: resolveCacheDir(env),
|
|
4717
|
-
hostSource
|
|
4718
|
-
},
|
|
4719
|
-
warnings
|
|
4720
|
-
};
|
|
4721
|
-
}
|
|
4722
|
-
function resolveCacheDir(env = process.env) {
|
|
4723
|
-
return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(resolveCacheHome(env), "harness", "attachments");
|
|
4724
|
-
}
|
|
4725
|
-
function absolute(value) {
|
|
4726
|
-
return value !== void 0 && value !== "" && isAbsolute3(value) ? value : null;
|
|
4727
|
-
}
|
|
4728
|
-
function resolveCacheHome(env = process.env) {
|
|
4729
|
-
return absolute(env["XDG_CACHE_HOME"]) ?? join4(homedir3() || tmpdir(), ".cache");
|
|
4730
|
-
}
|
|
4731
|
-
function normalize(url) {
|
|
4732
|
-
let parsed;
|
|
4733
|
-
try {
|
|
4734
|
-
parsed = new URL(url);
|
|
4735
|
-
} catch {
|
|
4736
|
-
return null;
|
|
4737
|
-
}
|
|
4738
|
-
const port = parsed.port !== "" ? parsed.port : parsed.protocol === "https:" ? "443" : parsed.protocol === "http:" ? "80" : "";
|
|
4739
|
-
return JSON.stringify([parsed.protocol, parsed.hostname, port, parsed.pathname.replace(/\/+$/, "")]);
|
|
4740
|
-
}
|
|
4741
|
-
function validateBaseUrl(baseUrl) {
|
|
4742
|
-
let parsed;
|
|
4743
|
-
try {
|
|
4744
|
-
parsed = new URL(baseUrl);
|
|
4745
|
-
} catch {
|
|
4746
|
-
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
4747
|
-
}
|
|
4748
|
-
if (parsed.username !== "" || parsed.password !== "") {
|
|
4749
|
-
return { ok: false, message: "HARNESS_API_URL must not embed credentials (user:pass@) \u2014 use HARNESS_API_TOKEN" };
|
|
4750
|
-
}
|
|
4751
|
-
if (parsed.search !== "" || parsed.hash !== "") {
|
|
4752
|
-
return { ok: false, message: `HARNESS_API_URL must not carry a query or fragment, got: ${baseUrl}` };
|
|
4753
|
-
}
|
|
4754
|
-
if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
|
|
4755
|
-
if (parsed.protocol === "http:") {
|
|
4756
|
-
if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) return { ok: true };
|
|
4757
|
-
return {
|
|
4758
|
-
ok: false,
|
|
4759
|
-
message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost only.`
|
|
4760
|
-
};
|
|
4761
|
-
}
|
|
4762
|
-
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
4763
|
-
}
|
|
4764
|
-
function loginHelp(baseUrl, name = invokedName()) {
|
|
4765
|
-
return `Not signed in to ${baseUrl}.
|
|
4766
|
-
|
|
4767
|
-
Interactively, in a terminal (not from inside an agent \u2014 the browser
|
|
4768
|
-
tab needs someone to see it):
|
|
4769
|
-
|
|
4770
|
-
${name} login
|
|
4771
|
-
|
|
4772
|
-
For agents and CI, mint an API token (admin \u2192 API tokens) with the
|
|
4773
|
-
harness:read scope \u2014 plus harness:write only where the workflow
|
|
4774
|
-
creates or closes \u2014 set an expiry, and export it:
|
|
4775
|
-
|
|
4776
|
-
export HARNESS_API_TOKEN=aone_...
|
|
4777
|
-
|
|
4778
|
-
An outside expert uses the pxc_ credential minted in their expert view:
|
|
4779
|
-
export it the same way, or paste it into \`${name} login --no-browser\`.
|
|
4780
|
-
`;
|
|
4781
|
-
}
|
|
4782
|
-
function blankToNull(value) {
|
|
4783
|
-
if (value === void 0 || value.trim() === "") return null;
|
|
4784
|
-
return value;
|
|
4785
|
-
}
|
|
4786
|
-
|
|
4787
3667
|
export {
|
|
4788
|
-
HttpError,
|
|
4789
|
-
UpgradeRequiredError,
|
|
4790
|
-
TransportError,
|
|
4791
|
-
UsageError,
|
|
4792
|
-
LocalError,
|
|
4793
|
-
CommandError,
|
|
4794
|
-
EXPERT_ASSIGNMENTS_PATH,
|
|
4795
|
-
isExpertToken,
|
|
4796
|
-
EXPERT_LAPSED,
|
|
4797
|
-
explainExpertRefusal,
|
|
4798
|
-
SESSION_PATH,
|
|
4799
|
-
isLoginCredential,
|
|
4800
|
-
Client,
|
|
4801
|
-
clean,
|
|
4802
|
-
json,
|
|
4803
|
-
expertSummary,
|
|
4804
|
-
errorDetails,
|
|
4805
3668
|
MAX_IDEMPOTENCY_KEY_BYTES,
|
|
4806
3669
|
IDEMPOTENCY_KEY_FORMAT,
|
|
4807
3670
|
requireIdentifiers,
|
|
@@ -4850,7 +3713,7 @@ export {
|
|
|
4850
3713
|
linkAdd,
|
|
4851
3714
|
linkUpdate,
|
|
4852
3715
|
linkRm,
|
|
4853
|
-
|
|
3716
|
+
labelList,
|
|
4854
3717
|
labelCreate,
|
|
4855
3718
|
teamCreate,
|
|
4856
3719
|
teamList,
|
|
@@ -4913,26 +3776,18 @@ export {
|
|
|
4913
3776
|
PLAN_EXECUTORS,
|
|
4914
3777
|
CONDITION_KINDS,
|
|
4915
3778
|
GATE_OWNER_ROLES,
|
|
3779
|
+
PLAN_STATUSES,
|
|
3780
|
+
PLAN_LIST_MAX_LIMIT,
|
|
4916
3781
|
readPlanFile,
|
|
4917
3782
|
planCreate,
|
|
3783
|
+
requirePlanListArgs,
|
|
3784
|
+
planList,
|
|
4918
3785
|
planShow,
|
|
4919
3786
|
planEdit,
|
|
4920
3787
|
planRelease,
|
|
4921
3788
|
planHold,
|
|
4922
3789
|
gateSet,
|
|
4923
3790
|
conditionList,
|
|
4924
|
-
|
|
4925
|
-
assignments
|
|
4926
|
-
defaultCredentialsPath,
|
|
4927
|
-
readCredentials,
|
|
4928
|
-
writeCredentials,
|
|
4929
|
-
deleteCredentials,
|
|
4930
|
-
findRepoConfig,
|
|
4931
|
-
isHostTrusted,
|
|
4932
|
-
trustHost,
|
|
4933
|
-
DEFAULT_BASE_URL,
|
|
4934
|
-
resolveConfig,
|
|
4935
|
-
resolveCacheHome,
|
|
4936
|
-
validateBaseUrl,
|
|
4937
|
-
loginHelp
|
|
3791
|
+
requirePlanRef,
|
|
3792
|
+
assignments
|
|
4938
3793
|
};
|