@allocator-one/harness-cli 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2258 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/core/errors.ts
4
- var HttpError = class extends Error {
5
- constructor(status, message, body = {}, retryAfter = null) {
6
- super(message);
7
- this.status = status;
8
- this.body = body;
9
- this.retryAfter = retryAfter;
10
- this.name = "HttpError";
11
- }
12
- status;
13
- body;
14
- retryAfter;
15
- };
16
- var UpgradeRequiredError = class extends HttpError {
17
- constructor(message, body = {}) {
18
- super(426, message, body);
19
- this.name = "UpgradeRequiredError";
20
- }
21
- };
22
- var TransportError = class extends Error {
23
- constructor(message) {
24
- super(message);
25
- this.name = "TransportError";
26
- }
27
- };
28
- var UsageError = class extends Error {
29
- constructor(message) {
30
- super(message);
31
- this.name = "UsageError";
32
- }
33
- };
34
- var LocalError = class extends Error {
35
- constructor(message) {
36
- super(message);
37
- this.name = "LocalError";
38
- }
39
- };
40
- var CommandError = class extends Error {
41
- constructor(message) {
42
- super(message);
43
- this.name = "CommandError";
44
- }
45
- };
46
-
47
- // src/version.ts
48
- import { readFileSync } from "fs";
49
- var pkg = JSON.parse(
50
- readFileSync(new URL("../package.json", import.meta.url), "utf8")
51
- );
52
- var CLI_VERSION = pkg.version;
53
-
54
- // src/core/client.ts
55
- var RECEIVE_TIMEOUT_MS = 3e4;
56
- var UPLOAD_RECEIVE_TIMEOUT_MS = 6e4;
57
- var UPLOAD_FLOOR_BYTES_PER_MS = 100;
58
- function uploadTimeoutMs(bodyBytes) {
59
- return UPLOAD_RECEIVE_TIMEOUT_MS + Math.ceil(bodyBytes / UPLOAD_FLOOR_BYTES_PER_MS);
60
- }
61
- var DEFAULT_MESSAGES = {
62
- 401: "Missing or invalid token (check HARNESS_API_TOKEN)",
63
- 403: "Token not permitted (scope or admin gate)",
64
- 404: "Not found",
65
- 409: "Conflict",
66
- 422: "Validation error",
67
- 426: "CLI too old for this API \u2014 upgrade: npm i -g @allocator-one/harness-cli@latest",
68
- 429: "Rate limited \u2014 retry later"
69
- };
70
- var Client = class {
71
- constructor(config) {
72
- this.config = config;
73
- this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
74
- }
75
- config;
76
- fetchImpl;
77
- get cliVersion() {
78
- return CLI_VERSION;
79
- }
80
- get baseUrl() {
81
- return this.config.baseUrl;
82
- }
83
- async get(path2, params) {
84
- return this.json(await this.raw("GET", path2, { params }));
85
- }
86
- async post(path2, body = {}, options = {}) {
87
- return this.json(
88
- await this.raw("POST", path2, {
89
- json: this.withActor(body),
90
- idempotencyKey: options.idempotencyKey
91
- })
92
- );
93
- }
94
- async patch(path2, body) {
95
- return this.json(await this.raw("PATCH", path2, { json: this.withActor(body) }));
96
- }
97
- async put(path2, body) {
98
- return this.json(await this.raw("PUT", path2, { json: this.withActor(body) }));
99
- }
100
- async delete(path2) {
101
- return this.json(await this.raw("DELETE", path2, {}));
102
- }
103
- /**
104
- * Posts multipart/form-data — the one endpoint that does not take JSON.
105
- * Shares auth, the version header, and error mapping with the JSON verbs,
106
- * but never the actor: there is no JSON body to carry an `actor` field,
107
- * and the server's file contract is the form itself.
108
- */
109
- async postMultipart(path2, form, bodyBytes = 0) {
110
- return this.json(await this.raw("POST", path2, { form, timeoutMs: uploadTimeoutMs(bodyBytes) }));
111
- }
112
- /** Binary download (attachments); non-2xx still maps through the JSON error envelope. */
113
- async getBytes(path2) {
114
- const response = await this.raw("GET", path2, {});
115
- if (!response.ok) throw await this.toHttpError(response);
116
- try {
117
- return new Uint8Array(await response.arrayBuffer());
118
- } catch (error) {
119
- throw new TransportError(transportMessage(error));
120
- }
121
- }
122
- /**
123
- * The server's API version, for `version`'s server side. The header rides
124
- * on every response — the versioning plug runs before auth — so even a
125
- * 401 answers.
126
- */
127
- async serverVersion() {
128
- const response = await this.raw("GET", "/api/v1/stats", {});
129
- return response.headers.get("x-harness-api-version") ?? "unknown";
130
- }
131
- // Only an explicitly configured actor is sent, and never over one the
132
- // caller already put in the body.
133
- withActor(body) {
134
- if (this.config.actor === null || "actor" in body) return body;
135
- return { ...body, actor: this.config.actor };
136
- }
137
- async raw(method, path2, options) {
138
- const url = new URL(this.config.baseUrl + path2);
139
- for (const [key, value] of Object.entries(options.params ?? {})) {
140
- if (value !== void 0) url.searchParams.set(key, String(value));
141
- }
142
- const headers = {
143
- authorization: `Bearer ${this.config.token}`,
144
- "x-harness-cli-version": CLI_VERSION
145
- };
146
- if (options.idempotencyKey !== void 0) {
147
- headers["idempotency-key"] = options.idempotencyKey;
148
- }
149
- let body;
150
- if (options.json !== void 0) {
151
- headers["content-type"] = "application/json";
152
- body = JSON.stringify(options.json);
153
- } else if (options.form !== void 0) {
154
- body = options.form;
155
- }
156
- try {
157
- return await this.fetchImpl(url, {
158
- method,
159
- headers,
160
- body,
161
- signal: AbortSignal.timeout(options.timeoutMs ?? RECEIVE_TIMEOUT_MS),
162
- redirect: "manual"
163
- });
164
- } catch (error) {
165
- throw new TransportError(transportMessage(error));
166
- }
167
- }
168
- async json(response) {
169
- if (!response.ok) throw await this.toHttpError(response);
170
- let text;
171
- try {
172
- text = await response.text();
173
- } catch (error) {
174
- throw new TransportError(transportMessage(error));
175
- }
176
- if (text === "") return { data: void 0 };
177
- let parsed;
178
- try {
179
- parsed = JSON.parse(text);
180
- } catch {
181
- throw new TransportError(`the API answered with a non-JSON body (HTTP ${response.status})`);
182
- }
183
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
184
- throw new TransportError(`the API answered with an invalid JSON envelope (HTTP ${response.status})`);
185
- }
186
- return parsed;
187
- }
188
- async toHttpError(response) {
189
- let parsed = {};
190
- try {
191
- const candidate = JSON.parse(await response.text());
192
- if (typeof candidate === "object" && candidate !== null) {
193
- parsed = candidate;
194
- }
195
- } catch {
196
- }
197
- const fromBody = typeof parsed["message"] === "string" ? parsed["message"] : null;
198
- const message = fromBody ?? DEFAULT_MESSAGES[response.status] ?? `HTTP ${response.status}`;
199
- if (response.status === 426) return new UpgradeRequiredError(message, parsed);
200
- return new HttpError(response.status, message, parsed, response.headers.get("retry-after"));
201
- }
202
- };
203
- function transportMessage(error) {
204
- if (error instanceof Error) {
205
- if (error.name === "TimeoutError" || error.name === "AbortError") return "request timed out";
206
- const cause = error.cause;
207
- if (cause instanceof Error && cause.message !== "") return cause.message;
208
- return error.message;
209
- }
210
- return String(error);
211
- }
212
-
213
- // src/core/output.ts
214
- var TITLE_WIDTH = 70;
215
- var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
216
- function graphemes(text) {
217
- return [...segmenter.segment(text)].map((piece) => piece.segment);
218
- }
219
- function graphemeLength(text) {
220
- return graphemes(text).length;
221
- }
222
- function padGraphemes(text, width) {
223
- const shortfall = width - graphemeLength(text);
224
- return shortfall > 0 ? text + " ".repeat(shortfall) : text;
225
- }
226
- function clean(value) {
227
- if (value === null || value === void 0) return "";
228
- return String(value).replace(/[\x00-\x1F\x7F\u0080-\u009F]/gu, "");
229
- }
230
- function json(term) {
231
- return JSON.stringify(term) + "\n";
232
- }
233
- function issuesTable(issues) {
234
- if (issues.length === 0) return "No issues.\n";
235
- const rows = issues.map((issue) => [
236
- str(issue["identifier"]),
237
- str(path(issue, "state", "name")) || "-",
238
- str(issue["priority_label"]) || "-",
239
- str(path(issue, "assignee", "name")) || "-",
240
- truncate(issue["title"])
241
- ]);
242
- return table(["ID", "STATE", "PRIORITY", "ASSIGNEE", "TITLE"], rows);
243
- }
244
- function issueDetail(issue, images = []) {
245
- const header = `${str(issue["identifier"])} \xB7 ${clean(issue["title"])}
246
- `;
247
- const fields = [
248
- ["State", path(issue, "state", "name")],
249
- ["Priority", issue["priority_label"]],
250
- ["Assignee", path(issue, "assignee", "name")],
251
- ["Team", path(issue, "team", "key")],
252
- ["Labels", labelsLine(issue["labels"])],
253
- ["Blocked", blockedLine(issue)],
254
- ["Due", issue["due_date"]],
255
- ["Updated", issue["updated_at"]]
256
- ];
257
- const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
258
- `).join("");
259
- const sections = [
260
- ["Description", issue["description"]],
261
- ["Design", issue["design"]],
262
- ["Acceptance criteria", issue["acceptance_criteria"]],
263
- ["Notes", issue["notes"]]
264
- ];
265
- const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
266
- ${title}:
267
- ${indent(String(text))}
268
- `).join("");
269
- return header + fieldLines + sectionText + relationsSection(issue["relations"]) + childrenSection(issue["children"]) + commentsSection(issue["comments"]) + imagesSection(images);
270
- }
271
- function activityEntries(entries) {
272
- if (entries.length === 0) return "No activity.\n";
273
- return entries.map(activityEntry).join("\n");
274
- }
275
- function activityEntry(entry) {
276
- switch (entry["kind"]) {
277
- case "comment":
278
- return activityComment(entry);
279
- case "state_event":
280
- return activityStateEvent(entry);
281
- case "assignee_event":
282
- return activityAssigneeEvent(entry);
283
- default:
284
- throw new CommandError(`Unknown activity entry kind: ${clean(entry["kind"])}`);
285
- }
286
- }
287
- function activityComment(entry) {
288
- const comment2 = requiredRecord(entry["comment"], "activity comment");
289
- const authorValue = comment2["author"];
290
- const author = authorValue === null ? "unknown" : optionalLabel(requiredRecord(authorValue, "comment author")["name"], "unknown", "comment author name");
291
- const deleted = comment2["deleted"];
292
- if (typeof deleted !== "boolean") throw new CommandError("Invalid comment deleted flag.");
293
- const body = activityCommentBody(comment2["body"], deleted);
294
- const text = cleanMultiline(body).split("\n").map((line) => " " + line).join("\n");
295
- return `[${clean(requiredString(entry["occurred_at"], "activity occurrence time"))}] ${clean(author)} (${clean(requiredString(comment2["id"], "comment id"))}):
296
- ${text}
297
- `;
298
- }
299
- function activityCommentBody(value, deleted) {
300
- if (deleted) return "[deleted comment]";
301
- if (value === null || value === "") return "[comment has no text]";
302
- return requiredString(value, "comment body");
303
- }
304
- function activityStateEvent(entry) {
305
- const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
306
- const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
307
- let description;
308
- switch (entry["action"]) {
309
- case "created":
310
- description = `${actor} created the issue in ${stateName(entry["to_state"], "created to_state")}`;
311
- break;
312
- case "transition":
313
- description = `${actor} moved the issue from ${nullableStateName(entry["from_state"], "transition from_state")} to ${stateName(entry["to_state"], "transition to_state")}`;
314
- break;
315
- case "milestone":
316
- description = `${actor} recorded the ${stateName(entry["to_state"], "milestone to_state")} milestone`;
317
- break;
318
- default:
319
- throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
320
- }
321
- return `[${clean(requiredString(entry["occurred_at"], "activity occurrence time"))}] ${description} \xB7 ${provenance}
322
- `;
323
- }
324
- function activityAssigneeEvent(entry) {
325
- const actor = activityActor(requiredRecord(entry["actor"], "activity actor"));
326
- const provenance = activityProvenance(requiredRecord(entry["provenance"], "activity provenance"));
327
- let description;
328
- switch (entry["action"]) {
329
- case "assigned":
330
- description = `${actor} assigned the issue to ${assigneeName(entry["to_assignee"])}`;
331
- break;
332
- case "reassigned":
333
- description = `${actor} reassigned the issue from ${assigneeName(entry["from_assignee"])} to ${assigneeName(entry["to_assignee"])}`;
334
- break;
335
- case "unassigned":
336
- description = `${actor} unassigned the issue from ${assigneeName(entry["from_assignee"])}`;
337
- break;
338
- default:
339
- throw new CommandError(`Unknown activity action: ${clean(entry["action"])}`);
340
- }
341
- return `[${clean(requiredString(entry["occurred_at"], "activity occurrence time"))}] ${description} - ${provenance}
342
- `;
343
- }
344
- function assigneeName(value) {
345
- const assignee = requiredRecord(value, "activity assignee");
346
- if (assignee["kind"] === "unknown") return "Unknown assignee";
347
- return activityActor(assignee);
348
- }
349
- function activityActor(actor) {
350
- switch (actor["kind"]) {
351
- case "user":
352
- return optionalLabel(actor["name"], "Unknown actor", "activity actor name");
353
- case "system":
354
- return "System";
355
- case "deleted_user":
356
- return "Deleted user";
357
- case "unknown":
358
- return "Unknown actor";
359
- default:
360
- throw new CommandError(`Unknown activity actor kind: ${clean(actor["kind"])}`);
361
- }
362
- }
363
- function activityProvenance(provenance) {
364
- switch (provenance["kind"]) {
365
- case "live":
366
- case "import":
367
- case "backfill":
368
- return String(provenance["kind"]);
369
- default:
370
- throw new CommandError(`Unknown activity provenance: ${clean(provenance["kind"])}`);
371
- }
372
- }
373
- function stateName(value, field2) {
374
- return optionalLabel(requiredRecord(value, field2)["name"], "Unknown state", `${field2} name`);
375
- }
376
- function nullableStateName(value, field2) {
377
- return value === null ? "Unknown state" : stateName(value, field2);
378
- }
379
- function optionalLabel(value, fallback, field2) {
380
- if (value === null || value === "") return fallback;
381
- return clean(requiredString(value, field2));
382
- }
383
- function requiredRecord(value, field2) {
384
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
385
- throw new CommandError(`Invalid ${field2}.`);
386
- }
387
- return value;
388
- }
389
- function requiredString(value, field2) {
390
- if (typeof value !== "string" || value === "") throw new CommandError(`Invalid ${field2}.`);
391
- return value;
392
- }
393
- function stats(data) {
394
- const teams = data["teams"] ?? [];
395
- const totals = data["totals"] ?? {};
396
- const rows = teams.map((team) => [
397
- str(path(team, "team", "key")),
398
- str(team["open"]),
399
- str(team["started"]),
400
- str(team["blocked"]),
401
- str(team["ready"]),
402
- str(team["closed"])
403
- ]);
404
- const totalsRow = ["TOTAL", ...["open", "started", "blocked", "ready", "closed"].map((key) => str(totals[key]))];
405
- return table(["TEAM", "OPEN", "STARTED", "BLOCKED", "READY", "CLOSED"], [...rows, totalsRow]);
406
- }
407
- function table(headers, rows) {
408
- const all = [headers, ...rows.map((row) => row.map((cell) => clean(cell)))];
409
- const widths = headers.map((_, column) => Math.max(...all.map((row) => graphemeLength(row[column] ?? ""))));
410
- return all.map(
411
- (row) => row.map((cell, column) => padGraphemes(cell, widths[column] ?? 0)).join(" ").replace(/\s+$/, "") + "\n"
412
- ).join("");
413
- }
414
- function relationsSection(relations) {
415
- if (!Array.isArray(relations) || relations.length === 0) return "";
416
- const lines = relations.map((relation) => ` ${str(relation["direction"]).padEnd(14)}${str(relation["other_issue"])}
417
- `).join("");
418
- return "\nRelations:\n" + lines;
419
- }
420
- function childrenSection(children) {
421
- if (!Array.isArray(children) || children.length === 0) return "";
422
- const lines = children.map((child) => {
423
- const state = str(path(child, "state", "name")) || "-";
424
- return ` ${str(child["identifier"])} [${state}] ${truncate(child["title"])}
425
- `;
426
- }).join("");
427
- return "\nSub-issues:\n" + lines;
428
- }
429
- function commentsSection(comments2) {
430
- if (!Array.isArray(comments2) || comments2.length === 0) return "";
431
- const lines = comments2.map((comment2) => {
432
- const author = clean(path(comment2, "author", "name") ?? "unknown");
433
- return ` [${str(comment2["inserted_at"])}] ${author}: ${truncate(comment2["body"])}
434
- `;
435
- }).join("");
436
- return "\nComments:\n" + lines;
437
- }
438
- function imagesSection(images) {
439
- if (images.length === 0) return "";
440
- const lines = images.map((image) => ` [${image["source"]}] ${clean(image["path"])}
441
- `).join("");
442
- return "\nImages (attachments downloaded locally \u2014 read them, they are part of the issue):\n" + lines;
443
- }
444
- function labelsLine(labels) {
445
- if (!Array.isArray(labels) || labels.length === 0) return null;
446
- return labels.map((label) => clean(label["name"])).join(", ");
447
- }
448
- function blockedLine(issue) {
449
- if (issue["blocked"] === true && Array.isArray(issue["blocked_by"])) {
450
- return `yes, by ${issue["blocked_by"].map(String).join(", ")}`;
451
- }
452
- return null;
453
- }
454
- function indent(text) {
455
- return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
456
- }
457
- function cleanMultiline(text) {
458
- return text.split("\n").map((line) => clean(line)).join("\n");
459
- }
460
- function truncate(text) {
461
- if (text === null || text === void 0) return "";
462
- const cleaned = clean(text);
463
- const pieces = graphemes(cleaned);
464
- if (pieces.length > TITLE_WIDTH) {
465
- return pieces.slice(0, TITLE_WIDTH - 1).join("") + "\u2026";
466
- }
467
- return cleaned;
468
- }
469
- function str(value) {
470
- return value === null || value === void 0 ? "" : String(value);
471
- }
472
- function path(record, ...keys) {
473
- let current = record;
474
- for (const key of keys) {
475
- if (typeof current !== "object" || current === null) return null;
476
- current = current[key];
477
- }
478
- return current ?? null;
479
- }
480
-
481
- // src/commands/issues.ts
482
- import { openAsBlob, openSync, closeSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
483
- import { basename, extname } from "path";
484
-
485
- // src/core/attachments.ts
486
- import { chmodSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
487
- import { dirname, join } from "path";
488
- var REF = /\/(?:uploads\/harness|api\/v1)\/attachments\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/([A-Za-z0-9._-]*\.(?:png|jpe?g|webp|gif))(?![A-Za-z0-9._-])/gi;
489
- var SOURCES = [
490
- ["description", "description"],
491
- ["design", "design"],
492
- ["acceptance_criteria", "acceptance criteria"],
493
- ["notes", "notes"]
494
- ];
495
- var MAX_IMAGES_PER_SHOW = 20;
496
- var MAX_ATTACHMENT_BYTES = 25e6;
497
- function refs(issue) {
498
- const texts = SOURCES.map(([field2, source]) => [issue[field2], source]);
499
- const comments2 = Array.isArray(issue["comments"]) ? issue["comments"] : [];
500
- for (const comment2 of comments2) texts.push([comment2["body"], "comment"]);
501
- const found = [];
502
- const seen = /* @__PURE__ */ new Set();
503
- for (const [text, source] of texts) {
504
- if (typeof text !== "string" || text === "") continue;
505
- for (const match of text.matchAll(REF)) {
506
- const ref = {
507
- attachmentId: match[1].toLowerCase(),
508
- file: match[2],
509
- source
510
- };
511
- const key = `${ref.attachmentId}/${ref.file}`;
512
- if (!seen.has(key)) {
513
- seen.add(key);
514
- found.push(ref);
515
- }
516
- }
517
- }
518
- return found;
519
- }
520
- async function fetchImages(client, issue, cacheDir) {
521
- const allRefs = refs(issue);
522
- const wanted = allRefs.slice(0, MAX_IMAGES_PER_SHOW);
523
- const dropped = allRefs.slice(MAX_IMAGES_PER_SHOW);
524
- const images = [];
525
- const failures = [];
526
- for (const ref of wanted) {
527
- const result = await download(client, ref, cacheDir);
528
- if (result.ok) {
529
- images.push({
530
- attachment_id: ref.attachmentId,
531
- file: ref.file,
532
- path: result.path,
533
- source: ref.source
534
- });
535
- } else {
536
- failures.push(`${ref.file}: ${result.message}`);
537
- }
538
- }
539
- if (dropped.length > 0) {
540
- failures.push(
541
- `${dropped.length} further reference(s) skipped \u2014 the cap is ${MAX_IMAGES_PER_SHOW} images per show`
542
- );
543
- }
544
- return { images, failures };
545
- }
546
- async function download(client, ref, cacheDir) {
547
- const path2 = join(cacheDir, hostSegment(client.baseUrl), ref.attachmentId, ref.file);
548
- try {
549
- if (statSync(path2).size > 0) return { ok: true, path: path2 };
550
- } catch {
551
- }
552
- const bytes = await fetchBytes(client, ref);
553
- if (!bytes.ok) return bytes;
554
- return write(path2, bytes.bytes);
555
- }
556
- function hostSegment(baseUrl) {
557
- let host = "unknown-host";
558
- let port = null;
559
- try {
560
- const parsed = new URL(baseUrl);
561
- host = parsed.hostname.toLowerCase().replace(/[^a-z0-9.-]/g, "_");
562
- port = parsed.port === "" ? null : Number(parsed.port);
563
- } catch {
564
- }
565
- return port === null || port === 80 || port === 443 ? host : `${host}_${port}`;
566
- }
567
- async function fetchBytes(client, ref) {
568
- let bytes;
569
- try {
570
- bytes = await client.getBytes(`/api/v1/attachments/${ref.attachmentId}/${ref.file}`);
571
- } catch (error) {
572
- if (error instanceof HttpError) return { ok: false, message: `${error.message} (HTTP ${error.status})` };
573
- if (error instanceof TransportError) return { ok: false, message: error.message };
574
- throw error;
575
- }
576
- if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
577
- return { ok: false, message: `response is larger than the ${MAX_ATTACHMENT_BYTES}-byte attachment cap` };
578
- }
579
- if (!isImage(bytes)) return { ok: false, message: "response is not an image" };
580
- return { ok: true, bytes };
581
- }
582
- function isImage(bytes) {
583
- const startsWith = (prefix) => prefix.every((byte, index) => bytes[index] === byte);
584
- if (startsWith([137, 80, 78, 71, 13, 10, 26, 10])) return true;
585
- if (startsWith([255, 216, 255])) return true;
586
- if (startsWith([71, 73, 70, 56, 55, 97])) return true;
587
- if (startsWith([71, 73, 70, 56, 57, 97])) return true;
588
- if (startsWith([82, 73, 70, 70]) && bytes.length >= 12) {
589
- return bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80;
590
- }
591
- return false;
592
- }
593
- var partialCounter = 0;
594
- function write(path2, bytes) {
595
- const partial = `${path2}.partial.${process.pid}.${++partialCounter}`;
596
- try {
597
- mkdirSync(dirname(path2), { recursive: true });
598
- chmodSync(dirname(path2), 448);
599
- writeFileSync(partial, bytes);
600
- chmodSync(partial, 384);
601
- renameSync(partial, path2);
602
- return { ok: true, path: path2 };
603
- } catch (error) {
604
- rmSync(partial, { force: true });
605
- const detail = error instanceof Error ? error.message : String(error);
606
- return { ok: false, message: `cannot write ${path2}: ${detail}` };
607
- }
608
- }
609
-
610
- // src/core/pagination.ts
611
- function notices(meta, requestedPageSize, records) {
612
- return capNotice(meta, requestedPageSize) + truncationNotice(meta, records);
613
- }
614
- function capNotice(meta, requestedPageSize) {
615
- const served = meta?.page_size;
616
- if (typeof served === "number" && typeof requestedPageSize === "number" && requestedPageSize > served) {
617
- return `harness: --limit ${requestedPageSize} was capped at ${served} by the server.
618
- `;
619
- }
620
- return "";
621
- }
622
- function truncationNotice(meta, records) {
623
- const total = meta?.total;
624
- const page = meta?.page;
625
- const pages = meta?.total_pages;
626
- if (typeof total !== "number" || typeof page !== "number" || typeof pages !== "number") return "";
627
- const shown = records.length;
628
- if (shown < total) {
629
- return `harness: showing ${shown} of ${total} (page ${page} of ${pages}) \u2014 pass --all for the rest, or --page to step through.
630
- `;
631
- }
632
- return "";
633
- }
634
- async function fetchEveryPage(fetchPage) {
635
- const first = await fetchPage(1);
636
- const totalPages = pageCount(first.meta);
637
- if (totalPages === null) return first.data;
638
- let records = first.data;
639
- for (let page = 2; page <= totalPages; page++) {
640
- const body = await fetchPage(page);
641
- records = dedupe([...records, ...body.data]);
642
- }
643
- return records;
644
- }
645
- function pageCount(meta) {
646
- const total = meta?.total_pages;
647
- return typeof total === "number" && total > 0 ? total : null;
648
- }
649
- function dedupe(records) {
650
- const seen = /* @__PURE__ */ new Set();
651
- return records.filter((record) => {
652
- const id = record["id"];
653
- const key = id === void 0 || id === null ? `struct:${stableKey(record)}` : `id:${stableKey(id)}`;
654
- if (seen.has(key)) return false;
655
- seen.add(key);
656
- return true;
657
- });
658
- }
659
- function stableKey(value) {
660
- return JSON.stringify(
661
- value,
662
- (_field, nested) => nested !== null && typeof nested === "object" && !Array.isArray(nested) ? Object.fromEntries(Object.entries(nested).sort(([a], [b]) => a < b ? -1 : 1)) : nested
663
- );
664
- }
665
-
666
- // src/commands/issues.ts
667
- var PRIORITY_NAMES = { urgent: 1, high: 2, medium: 3, low: 4, none: 0 };
668
- var PRIORITY_RANK = { 1: 0, 2: 1, 3: 2, 4: 3, 0: 4 };
669
- var STATUS_STATE_TYPES = {
670
- open: "backlog,unstarted",
671
- in_progress: "started",
672
- closed: "completed,canceled"
673
- };
674
- var READY_CLAIM_ATTEMPTS = 10;
675
- var MAX_IDEMPOTENCY_KEY_BYTES = 200;
676
- var IDEMPOTENCY_KEY_FORMAT = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?(?![\s\S])/;
677
- var IDENTIFIER_FORMAT = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
678
- var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
679
- function isUuid(value) {
680
- return UUID_FORMAT.test(value);
681
- }
682
- function requireIdentifiers(identifiers) {
683
- const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
684
- if (bad.length > 0) {
685
- throw new UsageError(`not an issue identifier: ${bad.join(", ")} (expected e.g. ALL-4223)`);
686
- }
687
- }
688
- async function ready(ctx, opts) {
689
- const params = { ...listParams(ctx, opts), ready: true };
690
- const { records, notice } = await fetchIssues(ctx, params, opts);
691
- const rank = (issue) => PRIORITY_RANK[issue["priority"]] ?? 4;
692
- const sorted = [...records].sort((a, b) => rank(a) - rank(b));
693
- if (opts.claim === true) return claimFirst(ctx, sorted);
694
- return { json: sorted, human: issuesTable(sorted), notice };
695
- }
696
- async function blocked(ctx, opts) {
697
- const params = { ...listParams(ctx, opts), blocked: true };
698
- const { records, notice } = await fetchIssues(ctx, params, opts);
699
- return { json: records, human: issuesTable(records), notice };
700
- }
701
- async function list(ctx, opts) {
702
- const params = listParams(ctx, opts);
703
- const { records, notice } = await fetchIssues(ctx, params, opts);
704
- return { json: records, human: issuesTable(records), notice };
705
- }
706
- async function search(ctx, queryWords, opts) {
707
- if (queryWords.length === 0) throw new UsageError("usage: harness search <query>");
708
- const deprecation = opts.page !== void 0 || opts.all === true ? "--page/--all are ignored: search returns one ranked list, capped at 20" : void 0;
709
- let params = { q: queryWords.join(" ") };
710
- if (opts.limit !== void 0) params = { ...params, limit: opts.limit };
711
- const body = await ctx.client.get("/api/v1/issues/search", params);
712
- const meta = body.meta ?? {};
713
- const degraded = meta["mode"] === "lexical" ? "semantic search unavailable \u2014 showing lexical matches only" : void 0;
714
- const notice = [deprecation, degraded].filter(Boolean).join("; ") || void 0;
715
- return { json: body.data, human: issuesTable(body.data), notice };
716
- }
717
- async function show(ctx, identifier, opts) {
718
- requireIdentifiers([identifier]);
719
- const body = await ctx.client.get(`/api/v1/issues/${identifier}`, {
720
- include: "relations,children,comments"
721
- });
722
- const issue = body.data;
723
- let images = [];
724
- let failures = [];
725
- if (opts.images !== false) {
726
- ({ images, failures } = await fetchImages(ctx.client, issue, ctx.config.cacheDir));
727
- }
728
- const json2 = images.length === 0 ? issue : { ...issue, local_images: images };
729
- const human = issueDetail(issue, images);
730
- const notice = failures.map((failure) => `harness: image not downloaded: ${clean(failure)}
731
- `).join("");
732
- return notice === "" ? { json: json2, human } : { json: json2, human, notice };
733
- }
734
- var ACTIVITY_MAX_LIMIT = 100;
735
- async function activity(ctx, identifier, opts) {
736
- requireIdentifiers([identifier]);
737
- let page = await activityPage(ctx, identifier, opts.limit);
738
- let entries = page.entries;
739
- if (opts.all === true) {
740
- const pages = [page.entries];
741
- const seenCursors = /* @__PURE__ */ new Set();
742
- while (page.hasMore) {
743
- const cursor = page.nextCursor;
744
- if (seenCursors.has(cursor)) {
745
- throw new CommandError("Activity response repeated a cursor.");
746
- }
747
- seenCursors.add(cursor);
748
- page = await activityPage(ctx, identifier, opts.limit, cursor);
749
- pages.push(page.entries);
750
- }
751
- entries = pages.reverse().flat();
752
- }
753
- const notice = opts.all !== true && page.hasMore ? `harness: showing the newest ${entries.length} activity entries \u2014 pass --all for older activity.
754
- ` : void 0;
755
- return { json: entries, human: activityEntries(entries), notice };
756
- }
757
- async function activityPage(ctx, identifier, limit, before) {
758
- const body = await ctx.client.get(`/api/v1/issues/${identifier}/activity`, { limit, before });
759
- if (!Array.isArray(body.data)) throw new CommandError("Activity response data is not an array.");
760
- if (body.data.some((entry) => typeof entry !== "object" || entry === null || Array.isArray(entry))) {
761
- throw new CommandError("Activity response contains a non-object entry.");
762
- }
763
- const meta = body.meta;
764
- if (typeof meta !== "object" || meta === null) {
765
- throw new CommandError("Activity response has no pagination metadata.");
766
- }
767
- const hasMore = meta.has_more;
768
- const nextCursor = meta.next_cursor;
769
- if (typeof hasMore !== "boolean") {
770
- throw new CommandError("Activity response has an invalid has_more value.");
771
- }
772
- if (nextCursor !== null && typeof nextCursor !== "string") {
773
- throw new CommandError("Activity response has an invalid next_cursor value.");
774
- }
775
- if (hasMore) {
776
- if (nextCursor === null || nextCursor === "") {
777
- throw new CommandError("Activity response says older entries exist but provides no cursor.");
778
- }
779
- if (body.data.length === 0) {
780
- throw new CommandError("Activity response says older entries exist but contains no entries.");
781
- }
782
- return { entries: body.data, hasMore: true, nextCursor };
783
- }
784
- if (nextCursor !== null) {
785
- throw new CommandError("Activity response provides a cursor when no older entries exist.");
786
- }
787
- return { entries: body.data, hasMore: false, nextCursor: null };
788
- }
789
- async function stats2(ctx) {
790
- const body = await ctx.client.get("/api/v1/stats");
791
- return { json: body.data, human: stats(body.data) };
792
- }
793
- async function prime(ctx) {
794
- const statsBody = await ctx.client.get("/api/v1/stats");
795
- const readyBody = await ctx.client.get("/api/v1/issues", { ready: true, page_size: 10 });
796
- const json2 = { stats: statsBody.data, ready: readyBody.data };
797
- const human = "# Harness workflow context\n\n" + stats(statsBody.data) + "\nReady to work:\n" + issuesTable(readyBody.data) + `
798
- Workflow: harness ready \u2192 harness update <id> --claim \u2192 work \u2192
799
- harness close <id> --reason "Implemented in <commit-hash>" (response
800
- lists newly unblocked issues \u2014 surface them).
801
- Create tracked work BEFORE coding: harness create --title "\u2026" --type task.
802
- Dependencies: harness dep add <issue> <blocker> (blocker blocks issue).
803
- Before reporting done: close the issues you finished.
804
- `;
805
- return { json: json2, human };
806
- }
807
- async function version(config) {
808
- let server;
809
- if (config === null || config.baseUrl === "") {
810
- server = "unconfigured";
811
- } else {
812
- server = await new Client(config).serverVersion().catch(() => "unreachable");
813
- }
814
- return { json: { cli: CLI_VERSION, server }, human: `harness ${CLI_VERSION} (server API ${server})
815
- ` };
816
- }
817
- async function create(ctx, opts) {
818
- if (opts.title === void 0) {
819
- throw new UsageError("usage: harness create --title <title> [--team <key>] \u2026");
820
- }
821
- validateType(opts.type);
822
- const team = requiredTeam(ctx, opts.team);
823
- validateIdempotencyKey(opts.idempotencyKey);
824
- const priority = optionalPriority(opts.priority);
825
- const dueDate = optionalDueDate(opts.dueDate);
826
- const labels = requestedLabelNames(wantedLabels(opts));
827
- const labelIds = await prepareCreateLabelIds(ctx, team, labels);
828
- let body = { title: opts.title };
829
- body = putOpt(body, "description", opts.description);
830
- body = putOpt(body, "design", opts.design);
831
- body = putOpt(body, "notes", opts.notes);
832
- body = putOpt(body, "acceptance_criteria", opts.acceptance);
833
- body = putOpt(body, "parent", opts.parent);
834
- body = putPresent(body, "priority", priority);
835
- body = putDueDate(body, dueDate);
836
- body = putAssignee(body, opts.assignee);
837
- if (labelIds.length > 0) body["label_ids"] = labelIds;
838
- const created = await ctx.client.post(`/api/v1/teams/${team}/issues`, body, {
839
- idempotencyKey: opts.idempotencyKey
840
- });
841
- const issue = created.data;
842
- return {
843
- json: issue,
844
- human: `Created ${str2(issue["identifier"])}: ${clean(issue["title"])}
845
- `
846
- };
847
- }
848
- function validateIdempotencyKey(key) {
849
- if (key === void 0) return;
850
- if (key.trim() === "") throw new UsageError("idempotency key must not be blank");
851
- if (Buffer.byteLength(key, "utf8") > MAX_IDEMPOTENCY_KEY_BYTES) {
852
- throw new UsageError(`idempotency key must not exceed ${MAX_IDEMPOTENCY_KEY_BYTES} UTF-8 bytes`);
853
- }
854
- if (!IDEMPOTENCY_KEY_FORMAT.test(key)) {
855
- throw new UsageError("idempotency key must use printable ASCII and cannot start or end with a space");
856
- }
857
- }
858
- async function update(ctx, identifier, opts) {
859
- requireIdentifiers([identifier]);
860
- if (opts.notes !== void 0 && opts.appendNotes !== void 0) {
861
- throw new UsageError("--notes replaces and --append-notes appends \u2014 pass one, not both");
862
- }
863
- if (opts.claim === true && opts.assignee !== void 0) {
864
- throw new UsageError(
865
- "--claim takes the issue and starts it; --assignee only sets the field \u2014 pass one, not both (--claim to take it yourself, --assignee to hand it to someone)"
866
- );
867
- }
868
- const priority = optionalPriority(opts.priority);
869
- const dueDate = optionalDueDate(opts.dueDate);
870
- const addLabels = requestedLabelInputs(opts.addLabel ?? []);
871
- const removeLabels = requestedLabelInputs(opts.removeLabel ?? []);
872
- const labelPlan = addLabels.length > 0 || removeLabels.length > 0 ? await prepareLabelUpdate(ctx, identifier, addLabels, removeLabels) : null;
873
- let issue = labelPlan?.issue ?? null;
874
- let wrote = false;
875
- if (opts.claim === true) {
876
- const body = await ctx.client.post(`/api/v1/issues/${identifier}/claim`);
877
- issue = body.data;
878
- wrote = true;
879
- }
880
- let patch = {};
881
- patch = putOpt(patch, "title", opts.title);
882
- patch = putOpt(patch, "description", opts.description);
883
- patch = putOpt(patch, "design", opts.design);
884
- patch = putOpt(patch, "notes", opts.notes);
885
- patch = putOpt(patch, "acceptance_criteria", opts.acceptance);
886
- patch = putOpt(patch, "parent", opts.parent);
887
- patch = putPresent(patch, "priority", priority);
888
- patch = putDueDate(patch, dueDate);
889
- patch = putAssignee(patch, opts.assignee);
890
- if (opts.appendNotes !== void 0) {
891
- const current = await ctx.client.get(`/api/v1/issues/${identifier}`);
892
- const existing = current.data["notes"];
893
- const combined = existing === null || existing === void 0 || existing === "" ? opts.appendNotes : `${str2(existing)}
894
-
895
- ${opts.appendNotes}`;
896
- patch["notes"] = combined;
897
- }
898
- if (Object.keys(patch).length > 0) {
899
- const body = await ctx.client.patch(`/api/v1/issues/${identifier}`, patch);
900
- issue = body.data;
901
- wrote = true;
902
- }
903
- if (opts.state !== void 0) {
904
- issue = await setState(ctx, identifier, opts.state);
905
- wrote = true;
906
- }
907
- if (labelPlan?.changed === true) {
908
- const body = await ctx.client.put(`/api/v1/issues/${identifier}/labels`, {
909
- label_ids: labelPlan.ids
910
- });
911
- issue = body.data;
912
- wrote = true;
913
- }
914
- if (issue === null) {
915
- throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
916
- }
917
- const human = wrote ? `Updated ${str2(issue["identifier"])}
918
- ` : `Labels already up to date for ${str2(issue["identifier"])}
919
- `;
920
- return { json: issue, human };
921
- }
922
- function requireCloseArgs(identifiers, opts) {
923
- if (identifiers.length === 0) {
924
- throw new UsageError(
925
- "usage: harness close <id> [<id>\u2026] [--reason <text>] [--cancel | --duplicate-of <id>]"
926
- );
927
- }
928
- requireIdentifiers(identifiers);
929
- if (opts.duplicateOf === void 0) return;
930
- requireIdentifiers([opts.duplicateOf]);
931
- if (opts.cancel === true) {
932
- throw new UsageError("--duplicate-of and --cancel are mutually exclusive");
933
- }
934
- if (identifiers.length !== 1) {
935
- throw new UsageError("--duplicate-of accepts exactly one source issue");
936
- }
937
- }
938
- async function close(ctx, identifiers, opts) {
939
- requireCloseArgs(identifiers, opts);
940
- let body = {};
941
- body = putPresent(body, "reason", opts.reason);
942
- body = putPresent(body, "cancel", opts.cancel);
943
- body = putPresent(body, "duplicate_of", opts.duplicateOf);
944
- const results = [];
945
- for (const identifier of identifiers) {
946
- try {
947
- const response = await ctx.client.post(`/api/v1/issues/${identifier}/close`, body);
948
- results.push({ identifier, ok: true, response });
949
- } catch (error) {
950
- if (error instanceof HttpError || error instanceof TransportError) {
951
- results.push({ identifier, ok: false, error });
952
- } else {
953
- throw error;
954
- }
955
- }
956
- }
957
- const human = results.map((result) => closeLine(result, opts.duplicateOf)).join("");
958
- const json2 = results.map(
959
- (result) => result.ok ? {
960
- identifier: result.identifier,
961
- closed: true,
962
- ...opts.duplicateOf === void 0 ? {} : { duplicate_of: opts.duplicateOf },
963
- unblocked: result.response["unblocked"] ?? null
964
- } : { identifier: result.identifier, closed: false, error: errorText(result.error) }
965
- );
966
- const partial = results.some((result) => !result.ok);
967
- return partial ? { json: json2, human, partial: true } : { json: json2, human };
968
- }
969
- function closeLine(result, duplicateOf) {
970
- if (!result.ok) return `FAILED ${result.identifier}: ${errorText(result.error)}
971
- `;
972
- const closure = duplicateOf === void 0 ? `Closed ${result.identifier}` : `Closed ${result.identifier} as duplicate of ${duplicateOf}`;
973
- const unblocked = result.response["unblocked"];
974
- if (Array.isArray(unblocked) && unblocked.length > 0) {
975
- return `${closure} \u2014 unblocked: ${unblocked.map(String).join(", ")}
976
- `;
977
- }
978
- return `${closure}
979
- `;
980
- }
981
- async function archive(ctx, identifiers, opts) {
982
- if (identifiers.length === 0) {
983
- throw new UsageError("usage: harness archive <id> [<id>\u2026] [--restore]");
984
- }
985
- requireIdentifiers(identifiers);
986
- const archived = opts.restore !== true;
987
- const verb = archived ? "Archived" : "Restored";
988
- const results = [];
989
- for (const identifier of identifiers) {
990
- try {
991
- const body = await ctx.client.put(`/api/v1/issues/${identifier}/archive_state`, { archived });
992
- results.push({ identifier, ok: true, issue: body.data });
993
- } catch (error) {
994
- if (error instanceof HttpError || error instanceof TransportError) {
995
- results.push({ identifier, ok: false, error });
996
- } else {
997
- throw error;
998
- }
999
- }
1000
- }
1001
- const human = results.map(
1002
- (result) => result.ok ? `${verb} ${result.identifier}
1003
- ` : `FAILED ${result.identifier}: ${errorText(result.error)}
1004
- `
1005
- ).join("");
1006
- const json2 = results.map(
1007
- (result) => result.ok ? { identifier: result.identifier, archived } : { identifier: result.identifier, error: errorText(result.error) }
1008
- );
1009
- const partial = results.some((result) => !result.ok);
1010
- return partial ? { json: json2, human, partial: true } : { json: json2, human };
1011
- }
1012
- async function comments(ctx, identifier) {
1013
- requireIdentifiers([identifier]);
1014
- const body = await ctx.client.get(`/api/v1/issues/${identifier}/comments`);
1015
- if (body.data.length === 0) return { json: body.data, human: "No comments.\n" };
1016
- const human = body.data.map((comment2) => {
1017
- const author = clean((comment2["author"] ?? {})["name"] ?? "unknown");
1018
- const id = clean(comment2["id"] ?? "unknown id");
1019
- const text = cleanMultiline(str2(comment2["body"])).split("\n").map((line) => " " + line).join("\n");
1020
- return `[${str2(comment2["inserted_at"])}] ${author} (${id}):
1021
- ${text}
1022
- `;
1023
- }).join("\n");
1024
- return { json: body.data, human };
1025
- }
1026
- async function dep(ctx, verb, issue, blocker) {
1027
- requireIdentifiers([issue, blocker]);
1028
- if (verb === "add") {
1029
- const response = await ctx.client.post(`/api/v1/issues/${issue}/relations`, {
1030
- type: "blocked_by",
1031
- related_issue: blocker
1032
- });
1033
- return { json: response.data, human: `${blocker} now blocks ${issue}
1034
- ` };
1035
- }
1036
- if (verb === "rm") {
1037
- const body = await ctx.client.get(`/api/v1/issues/${issue}/relations`);
1038
- const relation = (body.data ?? []).find(
1039
- (candidate) => candidate["direction"] === "blocked_by" && candidate["other_issue"] === blocker
1040
- );
1041
- if (relation === void 0) throw new CommandError(`${blocker} does not block this issue.`);
1042
- await ctx.client.delete(`/api/v1/issues/${issue}/relations/${str2(relation["id"])}`);
1043
- return { json: { deleted: relation["id"] }, human: `${blocker} no longer blocks ${issue}
1044
- ` };
1045
- }
1046
- throw new UsageError("usage: harness dep add|rm <issue> <blocker> (the blocker blocks the issue)");
1047
- }
1048
- var RELATION_CREATE_TYPES = [
1049
- "blocks",
1050
- "blocked_by",
1051
- "related",
1052
- "duplicate",
1053
- "duplicate_of",
1054
- "duplicated_by"
1055
- ];
1056
- var RELATION_UPDATE_TYPES = ["blocks", "blocked_by", "related", "duplicate_of"];
1057
- function requireRelationType(type, allowed, operation) {
1058
- if (!allowed.includes(type)) {
1059
- throw new UsageError(`relation ${operation} type must be one of: ${allowed.join(", ")}`);
1060
- }
1061
- }
1062
- async function findRelation(ctx, issue, idOrIssue) {
1063
- const response = await ctx.client.get(`/api/v1/issues/${issue}/relations`);
1064
- const idMatch = response.data.find((candidate) => candidate["id"] === idOrIssue);
1065
- if (idMatch !== void 0) return idMatch;
1066
- const issueMatches = response.data.filter((candidate) => candidate["other_issue"] === idOrIssue);
1067
- if (issueMatches.length > 1) {
1068
- throw new CommandError(`multiple relations on ${issue} match ${idOrIssue}; use a relation id`);
1069
- }
1070
- if (issueMatches.length === 0) {
1071
- throw new CommandError(`no relation on ${issue} matches ${idOrIssue} (by id or related issue)`);
1072
- }
1073
- return issueMatches[0];
1074
- }
1075
- async function resolveRelationId(ctx, issue, idOrIssue) {
1076
- if (isUuid(idOrIssue)) return idOrIssue.toLowerCase();
1077
- const relation = await findRelation(ctx, issue, idOrIssue);
1078
- return String(relation["id"]);
1079
- }
1080
- async function relationList(ctx, issue) {
1081
- requireIdentifiers([issue]);
1082
- const response = await ctx.client.get(`/api/v1/issues/${issue}/relations`);
1083
- const human = response.data.length === 0 ? "No relations.\n" : table(
1084
- ["ID", "DIRECTION", "ISSUE"],
1085
- response.data.map((relation) => [
1086
- String(relation["id"] ?? ""),
1087
- String(relation["direction"] ?? ""),
1088
- String(relation["other_issue"] ?? "")
1089
- ])
1090
- );
1091
- return { json: response.data, human };
1092
- }
1093
- async function relationAdd(ctx, issue, relatedIssue, type) {
1094
- requireIdentifiers([issue, relatedIssue]);
1095
- requireRelationType(type, RELATION_CREATE_TYPES, "add");
1096
- const response = await ctx.client.post(`/api/v1/issues/${issue}/relations`, {
1097
- type,
1098
- related_issue: relatedIssue
1099
- });
1100
- return { json: response.data, human: `Related ${issue} to ${relatedIssue} as ${type}
1101
- ` };
1102
- }
1103
- async function relationUpdate(ctx, issue, idOrIssue, type) {
1104
- requireIdentifiers([issue]);
1105
- requireRelationType(type, RELATION_UPDATE_TYPES, "update");
1106
- const id = await resolveRelationId(ctx, issue, idOrIssue);
1107
- const response = await ctx.client.patch(`/api/v1/issues/${issue}/relations/${id}`, { type });
1108
- return { json: response.data, human: `Updated relation ${id} on ${issue} to ${type}
1109
- ` };
1110
- }
1111
- async function relationRm(ctx, issue, idOrIssue) {
1112
- requireIdentifiers([issue]);
1113
- const id = await resolveRelationId(ctx, issue, idOrIssue);
1114
- await ctx.client.delete(`/api/v1/issues/${issue}/relations/${id}`);
1115
- return { json: { deleted: id }, human: `Removed relation ${id} from ${issue}
1116
- ` };
1117
- }
1118
- async function comment(ctx, identifier, textWords, mentions) {
1119
- requireIdentifiers([identifier]);
1120
- if (textWords.length === 0) {
1121
- throw new UsageError("usage: harness comment <id> <text> [--mention NAME]\u2026");
1122
- }
1123
- const body = { body: textWords.join(" ") };
1124
- if (mentions.length > 0) body["mentions"] = mentions;
1125
- const response = await ctx.client.post(`/api/v1/issues/${identifier}/comments`, body);
1126
- return { json: response.data, human: `Commented on ${identifier}
1127
- ` };
1128
- }
1129
- async function editComment(ctx, identifier, commentId, textWords) {
1130
- requireIdentifiers([identifier]);
1131
- requireCommentId(commentId);
1132
- const body = textWords.join(" ");
1133
- if (body.trim() === "") {
1134
- throw new UsageError("the comment text is empty \u2014 nothing to edit");
1135
- }
1136
- const response = await ctx.client.patch(
1137
- `/api/v1/issues/${identifier}/comments/${commentId}`,
1138
- { body }
1139
- );
1140
- return {
1141
- json: response.data,
1142
- human: `Edited comment ${commentId} on ${identifier}
1143
- `
1144
- };
1145
- }
1146
- async function deleteComment(ctx, identifier, commentId) {
1147
- requireIdentifiers([identifier]);
1148
- requireCommentId(commentId);
1149
- const response = await ctx.client.delete(
1150
- `/api/v1/issues/${identifier}/comments/${commentId}`
1151
- );
1152
- return {
1153
- json: response.data,
1154
- human: `Deleted comment ${commentId} from ${identifier}
1155
- `
1156
- };
1157
- }
1158
- function requireCommentId(commentId) {
1159
- if (!UUID_FORMAT.test(commentId)) {
1160
- throw new UsageError(`not a comment id: ${commentId} (expected a UUID)`);
1161
- }
1162
- }
1163
- var MAX_IMAGE_BYTES = 25e6;
1164
- var MAX_VIDEO_BYTES = 1e8;
1165
- async function attachImage(ctx, path2) {
1166
- return uploadAttachment(ctx, path2, MAX_IMAGE_BYTES);
1167
- }
1168
- async function attachFile(ctx, path2) {
1169
- const maxBytes = extname(path2).toLowerCase() === ".mp4" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
1170
- return uploadAttachment(ctx, path2, maxBytes);
1171
- }
1172
- async function uploadAttachment(ctx, path2, maxBytes) {
1173
- attachableFile(path2, maxBytes);
1174
- const file = maxBytes === MAX_VIDEO_BYTES ? new File([await openAsBlob(path2)], basename(path2)) : new File([readFileSync2(path2)], basename(path2));
1175
- requireAttachmentSize(path2, file.size, maxBytes);
1176
- const form = new FormData();
1177
- form.set("file", file);
1178
- const body = await ctx.client.postMultipart("/api/v1/uploads", form, file.size);
1179
- const attachment = body.data;
1180
- const markdown = attachment["markdown"];
1181
- if (typeof markdown !== "string") throw new CommandError("the upload response carried no markdown reference");
1182
- return { json: attachment, human: clean(markdown) + "\n" };
1183
- }
1184
- function attachableFile(path2, maxBytes) {
1185
- let fileStats;
1186
- try {
1187
- fileStats = statSync2(path2);
1188
- } catch (error) {
1189
- throw new LocalError(`cannot read ${path2}: ${posixDetail(error)}`);
1190
- }
1191
- if (!fileStats.isFile()) {
1192
- const kind = fileStats.isDirectory() ? "directory" : "special file";
1193
- throw new LocalError(`${path2} is a ${kind}, not a file.`);
1194
- }
1195
- requireAttachmentSize(path2, fileStats.size, maxBytes);
1196
- try {
1197
- closeSync(openSync(path2, "r"));
1198
- } catch (error) {
1199
- throw new LocalError(`cannot read ${path2}: ${posixDetail(error)}`);
1200
- }
1201
- }
1202
- function requireAttachmentSize(path2, size, maxBytes) {
1203
- if (size > maxBytes) {
1204
- throw new LocalError(`${path2} is too large: ${size} bytes, and attachments are capped at ${maxBytes}.`);
1205
- }
1206
- }
1207
- function posixDetail(error) {
1208
- const code = error.code;
1209
- if (code === "ENOENT") return "no such file or directory";
1210
- if (code === "EACCES") return "permission denied";
1211
- return error instanceof Error ? error.message : String(error);
1212
- }
1213
- async function claimFirst(ctx, issues) {
1214
- for (const issue of issues.slice(0, READY_CLAIM_ATTEMPTS)) {
1215
- try {
1216
- const body = await ctx.client.post(`/api/v1/issues/${str2(issue["identifier"])}/claim`);
1217
- const claimed = body.data;
1218
- return {
1219
- json: claimed,
1220
- human: `Claimed ${str2(claimed["identifier"])}: ${clean(claimed["title"])}
1221
- `
1222
- };
1223
- } catch (error) {
1224
- if (error instanceof HttpError && error.status === 409) continue;
1225
- throw error;
1226
- }
1227
- }
1228
- throw new CommandError("No claimable ready issues.");
1229
- }
1230
- async function setState(ctx, identifier, stateName2) {
1231
- const showBody = await ctx.client.get(`/api/v1/issues/${identifier}`);
1232
- const teamKey = str2((showBody.data["team"] ?? {})["key"]);
1233
- const statesBody = await ctx.client.get(
1234
- `/api/v1/teams/${teamKey}/workflow_states`
1235
- );
1236
- const wanted = stateName2.toLowerCase();
1237
- const state = statesBody.data.find(
1238
- (candidate) => str2(candidate["name"]).toLowerCase() === wanted || str2(candidate["type"]) === wanted
1239
- );
1240
- if (state === void 0) {
1241
- const names = statesBody.data.map((candidate) => str2(candidate["name"])).join(", ");
1242
- throw new CommandError(`No workflow state matches "${stateName2}". States: ${names}`);
1243
- }
1244
- const body = await ctx.client.put(`/api/v1/issues/${identifier}/state`, { state_id: state["id"] });
1245
- return body.data;
1246
- }
1247
- var ISSUE_TYPES = ["task", "bug", "feature", "epic"];
1248
- function wantedLabels(opts) {
1249
- const typeLabels = opts.type === void 0 ? [] : [`type/${opts.type}`];
1250
- return [...typeLabels, ...opts.label ?? []];
1251
- }
1252
- function validateType(type) {
1253
- if (type !== void 0 && !ISSUE_TYPES.includes(type)) {
1254
- throw new UsageError(`not an issue type: ${type} (expected ${ISSUE_TYPES.join(", ")})`);
1255
- }
1256
- }
1257
- function requestedLabelNames(names) {
1258
- const unique = /* @__PURE__ */ new Map();
1259
- for (const name of requestedLabelInputs(names)) {
1260
- const normalized = normalizeLabelName(name);
1261
- if (!unique.has(normalized)) unique.set(normalized, name);
1262
- }
1263
- return [...unique.values()];
1264
- }
1265
- function requestedLabelInputs(values) {
1266
- return values.map((value) => {
1267
- const trimmed = value.trim();
1268
- if (trimmed === "") throw new UsageError("label names and ids cannot be empty");
1269
- return trimmed;
1270
- });
1271
- }
1272
- function normalizeLabelName(value) {
1273
- return str2(value).trim().toLowerCase();
1274
- }
1275
- async function prepareCreateLabelIds(ctx, teamKey, names) {
1276
- if (names.length === 0) return [];
1277
- const teams = await ctx.client.get("/api/v1/teams");
1278
- const target = teams.data.find(
1279
- (team) => str2(team["key"]).toLowerCase() === teamKey.toLowerCase()
1280
- );
1281
- if (target === void 0) throw new CommandError(`No team matches key "${teamKey}".`);
1282
- const teamId = str2(target["id"]);
1283
- if (teamId === "") throw new CommandError(`team ${teamKey} carried no id`);
1284
- return prepareApplicableLabelIds(ctx, teamId, names);
1285
- }
1286
- async function prepareApplicableLabelIds(ctx, teamId, names) {
1287
- let labels = (await ctx.client.get("/api/v1/labels")).data;
1288
- const ids = [];
1289
- for (const name of names) {
1290
- const resolved = await resolveOrCreateApplicableLabel(ctx, labels, name, teamId);
1291
- labels = resolved.catalog;
1292
- ids.push(labelId(resolved.label));
1293
- }
1294
- return [...new Set(ids)];
1295
- }
1296
- function applicableLabel(labels, input, teamId) {
1297
- const normalized = normalizeLabelName(input);
1298
- const named = labels.filter((label) => normalizeLabelName(label["name"]) === normalized);
1299
- const teamMatches = named.filter((label) => label["team_id"] === teamId);
1300
- if (teamMatches.length > 1) {
1301
- throw new CommandError(`more than one team label matches "${input}"; use a label id`);
1302
- }
1303
- if (teamMatches.length === 1) return teamMatches[0];
1304
- const workspaceMatches = named.filter(
1305
- (label) => label["team_id"] === null || label["team_id"] === void 0
1306
- );
1307
- if (workspaceMatches.length > 1) {
1308
- throw new CommandError(`more than one workspace label matches "${input}"; use a label id`);
1309
- }
1310
- return workspaceMatches[0];
1311
- }
1312
- async function resolveOrCreateApplicableLabel(ctx, catalog, name, teamId) {
1313
- const existing = applicableLabel(catalog, name, teamId);
1314
- if (existing !== void 0) return { label: existing, catalog };
1315
- try {
1316
- const created = await ctx.client.post("/api/v1/labels", {
1317
- name,
1318
- color: "gray"
1319
- });
1320
- return { label: created.data, catalog: [...catalog, created.data] };
1321
- } catch (error) {
1322
- if (!(error instanceof HttpError) || error.status !== 422) throw error;
1323
- const refreshed = (await ctx.client.get("/api/v1/labels")).data;
1324
- const winner = applicableLabel(refreshed, name, teamId);
1325
- if (winner === void 0) throw error;
1326
- return { label: winner, catalog: refreshed };
1327
- }
1328
- }
1329
- function labelId(label) {
1330
- const id = str2(label["id"]);
1331
- if (id === "") throw new CommandError("the label response carried no id");
1332
- return id;
1333
- }
1334
- async function prepareLabelUpdate(ctx, identifier, addInputs, removeInputs) {
1335
- const issue = (await ctx.client.get(`/api/v1/issues/${identifier}`)).data;
1336
- const attached = Array.isArray(issue["labels"]) ? issue["labels"] : [];
1337
- const originalIds = attached.map(labelId);
1338
- const removed = /* @__PURE__ */ new Set();
1339
- for (const input of removeInputs) {
1340
- const idInput = UUID_FORMAT.test(input);
1341
- const exact = attached.find((label) => {
1342
- const id = labelId(label);
1343
- return idInput ? id.toLowerCase() === input.toLowerCase() : id === input;
1344
- });
1345
- if (exact !== void 0) {
1346
- removed.add(labelId(exact));
1347
- continue;
1348
- }
1349
- const normalized = normalizeLabelName(input);
1350
- const matches = attached.filter((label) => normalizeLabelName(label["name"]) === normalized);
1351
- if (matches.length > 1) {
1352
- throw new CommandError(`more than one attached label matches "${input}"; remove by label id`);
1353
- }
1354
- if (matches.length === 1) removed.add(labelId(matches[0]));
1355
- }
1356
- const ids = originalIds.filter((id) => !removed.has(id));
1357
- if (addInputs.length > 0) {
1358
- const team = issue["team"] ?? {};
1359
- const teamId = str2(team["id"]);
1360
- if (teamId === "") throw new CommandError(`issue ${identifier} carried no team id`);
1361
- let catalog = (await ctx.client.get("/api/v1/labels")).data;
1362
- for (const input of addInputs) {
1363
- const idInput = UUID_FORMAT.test(input);
1364
- let chosen = idInput ? catalog.find((label) => labelId(label).toLowerCase() === input.toLowerCase()) : void 0;
1365
- if (chosen !== void 0) {
1366
- const owner = chosen["team_id"];
1367
- if (owner !== null && owner !== void 0 && owner !== teamId) {
1368
- throw new CommandError(
1369
- `label ${input} belongs to another team and cannot be added to ${identifier}`
1370
- );
1371
- }
1372
- } else {
1373
- if (idInput) throw new CommandError(`No label matches id "${input}".`);
1374
- const resolved = await resolveOrCreateApplicableLabel(ctx, catalog, input, teamId);
1375
- chosen = resolved.label;
1376
- catalog = resolved.catalog;
1377
- }
1378
- const id = labelId(chosen);
1379
- if (!ids.includes(id)) ids.push(id);
1380
- }
1381
- }
1382
- return {
1383
- issue,
1384
- ids,
1385
- changed: ids.length !== originalIds.length || ids.some((id, index) => id !== originalIds[index])
1386
- };
1387
- }
1388
- async function fetchIssues(ctx, params, opts) {
1389
- if (opts.all === true && opts.page !== void 0) {
1390
- throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
1391
- }
1392
- if (opts.all === true) {
1393
- const records2 = await fetchEveryPage(
1394
- (page) => ctx.client.get("/api/v1/issues", { ...params, page })
1395
- );
1396
- return { records: records2, notice: "" };
1397
- }
1398
- const body = await ctx.client.get("/api/v1/issues", params);
1399
- const records = body.data;
1400
- const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
1401
- return { records, notice: notices(body.meta, requested, records) };
1402
- }
1403
- function listParams(ctx, opts) {
1404
- if (opts.type !== void 0 && opts.label !== void 0) {
1405
- throw new UsageError("--type is a label filter itself \u2014 pass either --type or --label, not both");
1406
- }
1407
- validateType(opts.type);
1408
- let params = withTeam({ page_size: opts.limit ?? 50, page: opts.page ?? 1 }, ctx, opts);
1409
- params = putParam(params, "search", opts.search);
1410
- params = putParam(params, "parent", opts.parent);
1411
- params = putParam(params, "label", opts.label);
1412
- params = putParam(params, "scope", opts.scope);
1413
- params = putParam(params, "assignee", opts.assignee);
1414
- params = putParam(params, "created_by", opts.createdBy);
1415
- params = putParam(params, "involved", opts.involved);
1416
- if (opts.type !== void 0) params = putParam(params, "label", `type/${opts.type}`);
1417
- if (opts.status !== void 0) {
1418
- const stateTypes = Object.hasOwn(STATUS_STATE_TYPES, opts.status) ? STATUS_STATE_TYPES[opts.status] : void 0;
1419
- if (stateTypes === void 0) {
1420
- throw new UsageError(`status must be one of: ${Object.keys(STATUS_STATE_TYPES).join(", ")}`);
1421
- }
1422
- params = putParam(params, "state_type", stateTypes);
1423
- }
1424
- const priority = optionalPriority(opts.priority);
1425
- if (priority !== null) params = putParam(params, "priority", priority);
1426
- return params;
1427
- }
1428
- function optionalPriority(value) {
1429
- if (value === void 0) return null;
1430
- const named = Object.hasOwn(PRIORITY_NAMES, value) ? PRIORITY_NAMES[value] : void 0;
1431
- if (named !== void 0) return named;
1432
- if (/^\d+$/.test(value)) {
1433
- throw new UsageError(
1434
- `Bare numeric priorities are refused: bd and harness disagree on what
1435
- the numbers mean, so a habit-typed number would silently file the
1436
- wrong priority.
1437
-
1438
- bd: 0=critical 1=high 2=medium 3=normal 4=backlog
1439
- harness: 0=none 1=urgent 2=high 3=medium 4=low
1440
-
1441
- Use a name instead: --priority urgent|high|medium|low|none
1442
- `
1443
- );
1444
- }
1445
- throw new UsageError("priority must be one of: urgent, high, medium, low, none");
1446
- }
1447
- function optionalDueDate(value) {
1448
- if (value === void 0) return null;
1449
- const trimmed = value.trim();
1450
- if (trimmed === "") return "clear";
1451
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
1452
- if (match === null) throw new UsageError(`not a date: ${trimmed} (expected YYYY-MM-DD)`);
1453
- const [, year, month, day] = match.map(Number);
1454
- const candidate = /* @__PURE__ */ new Date(0);
1455
- candidate.setUTCFullYear(year, month - 1, day);
1456
- const real = candidate.getUTCFullYear() === year && candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day;
1457
- if (!real) throw new UsageError(`not a date: ${trimmed} (no such day on the calendar)`);
1458
- return trimmed;
1459
- }
1460
- var TEAM_KEY_FORMAT = /^[A-Za-z][A-Za-z0-9]*$/;
1461
- function requiredTeam(ctx, team) {
1462
- const resolved = team ?? ctx.config.team ?? void 0;
1463
- if (resolved === void 0) throw new UsageError("No team given. Pass --team <key> or set HARNESS_TEAM.");
1464
- if (!TEAM_KEY_FORMAT.test(resolved)) throw new UsageError(`not a team key: ${resolved} (expected e.g. ALL)`);
1465
- return resolved;
1466
- }
1467
- function withTeam(params, ctx, opts) {
1468
- const team = opts.team ?? ctx.config.team ?? void 0;
1469
- return team === void 0 ? params : { ...params, team };
1470
- }
1471
- function putParam(params, key, value) {
1472
- return value === void 0 ? params : { ...params, [key]: value };
1473
- }
1474
- function putOpt(body, field2, value) {
1475
- return value === void 0 ? body : { ...body, [field2]: value };
1476
- }
1477
- function putPresent(body, key, value) {
1478
- if (value === null || value === void 0 || value === false) return body;
1479
- return { ...body, [key]: value };
1480
- }
1481
- function putDueDate(body, dueDate) {
1482
- if (dueDate === null) return body;
1483
- if (dueDate === "clear") return { ...body, due_date: null };
1484
- return { ...body, due_date: dueDate };
1485
- }
1486
- function putAssignee(body, assignee) {
1487
- if (assignee === void 0) return body;
1488
- return { ...body, assignee: assignee.trim() === "" ? null : assignee };
1489
- }
1490
- function errorText(error) {
1491
- if (error instanceof HttpError) return `${clean(error.message)} (HTTP ${error.status})`;
1492
- return `cannot reach the API: ${clean(error.message)}`;
1493
- }
1494
- function str2(value) {
1495
- return value === null || value === void 0 ? "" : String(value);
1496
- }
1497
-
1498
- // src/commands/extended.ts
1499
- var SAFE_ID = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
1500
- function requireSafeId(kind, id) {
1501
- if (!SAFE_ID.test(id)) throw new UsageError(`not a ${kind} id: ${id}`);
1502
- }
1503
- function str3(value) {
1504
- return value === null || value === void 0 ? "" : String(value);
1505
- }
1506
- function field(row, ...keys) {
1507
- let current = row;
1508
- for (const key of keys) {
1509
- if (typeof current !== "object" || current === null) return null;
1510
- current = current[key];
1511
- }
1512
- return current ?? null;
1513
- }
1514
- function putOpt2(body, key, value) {
1515
- return value === void 0 ? body : { ...body, [key]: value };
1516
- }
1517
- function putDate(body, key, raw) {
1518
- const parsed = optionalDueDate(raw);
1519
- if (parsed === null) return body;
1520
- return { ...body, [key]: parsed === "clear" ? null : parsed };
1521
- }
1522
- async function paginatedRead(ctx, path2, params, opts) {
1523
- if (opts.all === true && opts.page !== void 0) {
1524
- throw new UsageError("--all reads every page from the first \u2014 pass --all or --page, not both");
1525
- }
1526
- if (opts.all === true) {
1527
- const records = await fetchEveryPage((page) => ctx.client.get(path2, { ...params, page }));
1528
- return { records, notice: "" };
1529
- }
1530
- const body = await ctx.client.get(path2, params);
1531
- const requested = typeof params["page_size"] === "number" ? params["page_size"] : void 0;
1532
- return { records: body.data, notice: notices(body.meta, requested, body.data) };
1533
- }
1534
- async function projectList(ctx, opts) {
1535
- let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
1536
- if (opts.search !== void 0) params = { ...params, search: opts.search };
1537
- if (opts.state !== void 0) params = { ...params, state: opts.state };
1538
- const { records, notice } = await paginatedRead(ctx, "/api/v1/projects", params, opts);
1539
- const human = records.length === 0 ? "No projects.\n" : table(
1540
- ["ID", "STATE", "LEAD", "TARGET", "NAME"],
1541
- records.map((project) => [
1542
- str3(project["id"]),
1543
- str3(project["state"]) || "-",
1544
- str3(field(project, "lead", "name")) || "-",
1545
- str3(project["target_date"]) || "-",
1546
- str3(project["name"])
1547
- ])
1548
- );
1549
- return { json: records, human, notice };
1550
- }
1551
- async function projectShow(ctx, id) {
1552
- requireSafeId("project", id);
1553
- const body = await ctx.client.get(`/api/v1/projects/${id}`);
1554
- const project = body.data;
1555
- const fields = [
1556
- ["State", project["state"]],
1557
- ["Lead", field(project, "lead", "name")],
1558
- ["Start", project["start_date"]],
1559
- ["Target", project["target_date"]],
1560
- ["Updated", project["updated_at"]]
1561
- ];
1562
- const fieldLines = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([label, value]) => ` ${(label + ":").padEnd(12)}${clean(value)}
1563
- `).join("");
1564
- const sections = [
1565
- ["Summary", project["summary"]],
1566
- ["Description", project["description"]]
1567
- ];
1568
- const sectionText = sections.filter(([, text]) => text !== null && text !== void 0 && text !== "").map(([title, text]) => `
1569
- ${title}:
1570
- ${indent2(String(text))}
1571
- `).join("");
1572
- const milestones = Array.isArray(project["milestones"]) ? project["milestones"] : [];
1573
- const milestoneText = milestones.length === 0 ? "" : "\nMilestones:\n" + milestones.map((milestone) => ` ${(str3(milestone["target_date"]) || "-").padEnd(12)}${clean(milestone["name"])}
1574
- `).join("");
1575
- const human = `${str3(project["id"])} \xB7 ${clean(project["name"])}
1576
- ` + fieldLines + sectionText + milestoneText;
1577
- return { json: project, human };
1578
- }
1579
- function projectBody(opts) {
1580
- let body = {};
1581
- body = putOpt2(body, "name", opts.name);
1582
- body = putOpt2(body, "description", opts.description);
1583
- body = putOpt2(body, "summary", opts.summary);
1584
- body = putOpt2(body, "state", opts.state);
1585
- body = putOpt2(body, "lead_id", opts.lead);
1586
- body = putDate(body, "start_date", opts.startDate);
1587
- body = putDate(body, "target_date", opts.targetDate);
1588
- return body;
1589
- }
1590
- async function projectCreate(ctx, opts) {
1591
- if (opts.name === void 0) throw new UsageError("usage: harness project create --name <name> \u2026");
1592
- const body = await ctx.client.post("/api/v1/projects", projectBody(opts));
1593
- const project = body.data;
1594
- return { json: project, human: `Created project ${clean(project["name"])} (${str3(project["id"])})
1595
- ` };
1596
- }
1597
- async function projectUpdate(ctx, id, opts) {
1598
- requireSafeId("project", id);
1599
- const patch = projectBody(opts);
1600
- if (Object.keys(patch).length === 0) {
1601
- throw new UsageError("usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026");
1602
- }
1603
- const body = await ctx.client.patch(`/api/v1/projects/${id}`, patch);
1604
- return { json: body.data, human: `Updated project ${str3(body.data["id"])}
1605
- ` };
1606
- }
1607
- async function milestoneList(ctx, project) {
1608
- if (project === void 0) throw new UsageError("usage: harness milestone list --project <id>");
1609
- requireSafeId("project", project);
1610
- const body = await ctx.client.get(`/api/v1/projects/${project}/milestones`);
1611
- const human = body.data.length === 0 ? "No milestones.\n" : table(
1612
- ["ID", "TARGET", "NAME"],
1613
- body.data.map((milestone) => [
1614
- str3(milestone["id"]),
1615
- str3(milestone["target_date"]) || "-",
1616
- str3(milestone["name"])
1617
- ])
1618
- );
1619
- return { json: body.data, human };
1620
- }
1621
- async function milestoneCreate(ctx, opts) {
1622
- if (opts.project === void 0 || opts.name === void 0) {
1623
- throw new UsageError("usage: harness milestone create --project <id> --name <name> [--description TEXT] [--target-date YYYY-MM-DD]");
1624
- }
1625
- requireSafeId("project", opts.project);
1626
- let body = { name: opts.name };
1627
- body = putOpt2(body, "description", opts.description);
1628
- body = putDate(body, "target_date", opts.targetDate);
1629
- const response = await ctx.client.post(`/api/v1/projects/${opts.project}/milestones`, body);
1630
- const milestone = response.data;
1631
- return {
1632
- json: milestone,
1633
- human: `Created milestone ${clean(milestone["name"])} (${str3(milestone["id"])})
1634
- `
1635
- };
1636
- }
1637
- async function docList(ctx, opts) {
1638
- let params = { page_size: opts.limit ?? 50, page: opts.page ?? 1 };
1639
- if (opts.search !== void 0) params = { ...params, search: opts.search };
1640
- const { records, notice } = await paginatedRead(ctx, "/api/v1/documents", params, opts);
1641
- const human = records.length === 0 ? "No documents.\n" : table(
1642
- ["ID", "UPDATED", "TITLE"],
1643
- records.map((doc) => [str3(doc["id"]), str3(doc["updated_at"]) || "-", str3(doc["title"])])
1644
- );
1645
- return { json: records, human, notice };
1646
- }
1647
- async function docShow(ctx, id) {
1648
- requireSafeId("document", id);
1649
- const body = await ctx.client.get(`/api/v1/documents/${id}`);
1650
- const doc = body.data;
1651
- const content = doc["content"];
1652
- const human = `${str3(doc["id"])} \xB7 ${clean(doc["title"])}
1653
- Updated: ${clean(doc["updated_at"])}
1654
- ` + (content === null || content === void 0 || content === "" ? "" : "\n" + cleanMultiline(String(content)) + "\n");
1655
- return { json: doc, human };
1656
- }
1657
- async function docCreate(ctx, opts) {
1658
- if (opts.title === void 0) throw new UsageError("usage: harness doc create --title <title> [--content TEXT] [--project ID]");
1659
- let body = { title: opts.title };
1660
- body = putOpt2(body, "content", opts.content);
1661
- body = putOpt2(body, "project_id", opts.project);
1662
- const response = await ctx.client.post("/api/v1/documents", body);
1663
- const doc = response.data;
1664
- return { json: doc, human: `Created document ${clean(doc["title"])} (${str3(doc["id"])})
1665
- ` };
1666
- }
1667
- async function docUpdate(ctx, id, opts) {
1668
- requireSafeId("document", id);
1669
- let patch = {};
1670
- patch = putOpt2(patch, "title", opts.title);
1671
- patch = putOpt2(patch, "content", opts.content);
1672
- patch = putOpt2(patch, "project_id", opts.project);
1673
- if (Object.keys(patch).length === 0) {
1674
- throw new UsageError("usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026");
1675
- }
1676
- const response = await ctx.client.patch(`/api/v1/documents/${id}`, patch);
1677
- return { json: response.data, human: `Updated document ${str3(response.data["id"])}
1678
- ` };
1679
- }
1680
- async function linkList(ctx, issue) {
1681
- requireIdentifiers([issue]);
1682
- const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
1683
- const human = body.data.length === 0 ? "No links.\n" : table(
1684
- ["ID", "URL", "TITLE"],
1685
- body.data.map((link) => [str3(link["id"]), str3(link["url"]), str3(link["title"]) || "-"])
1686
- );
1687
- return { json: body.data, human };
1688
- }
1689
- async function linkAdd(ctx, issue, url, title) {
1690
- requireIdentifiers([issue]);
1691
- let body = { url };
1692
- body = putOpt2(body, "title", title);
1693
- const response = await ctx.client.post(`/api/v1/issues/${issue}/links`, body);
1694
- return { json: response.data, human: `Linked ${clean(url)} to ${issue}
1695
- ` };
1696
- }
1697
- async function findLink(ctx, issue, idOrUrl, allowDuplicateUrl = false) {
1698
- const body = await ctx.client.get(`/api/v1/issues/${issue}/links`);
1699
- const idMatch = body.data.find((candidate) => candidate["id"] === idOrUrl);
1700
- if (idMatch !== void 0) return idMatch;
1701
- const urlMatches = body.data.filter((candidate) => candidate["url"] === idOrUrl);
1702
- if (urlMatches.length > 1 && !allowDuplicateUrl) {
1703
- throw new CommandError(`multiple links on ${issue} match ${idOrUrl}; use a link id`);
1704
- }
1705
- if (urlMatches.length === 0) {
1706
- throw new CommandError(`no link on ${issue} matches ${idOrUrl} (by id or exact URL)`);
1707
- }
1708
- return urlMatches[0];
1709
- }
1710
- async function linkUpdate(ctx, issue, idOrUrl, opts) {
1711
- requireIdentifiers([issue]);
1712
- let patch = {};
1713
- patch = putOpt2(patch, "url", opts.url);
1714
- patch = putOpt2(patch, "title", opts.title);
1715
- if (Object.keys(patch).length === 0) {
1716
- throw new UsageError("usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>");
1717
- }
1718
- const id = isUuid(idOrUrl) ? idOrUrl.toLowerCase() : str3((await findLink(ctx, issue, idOrUrl))["id"]);
1719
- if (!isUuid(idOrUrl)) patch["expected_url"] = idOrUrl;
1720
- const response = await ctx.client.patch(`/api/v1/issues/${issue}/links/${id}`, patch);
1721
- return { json: response.data, human: `Updated link ${clean(response.data["url"])} on ${issue}
1722
- ` };
1723
- }
1724
- async function linkRm(ctx, issue, idOrUrl) {
1725
- requireIdentifiers([issue]);
1726
- const link = await findLink(ctx, issue, idOrUrl, true);
1727
- await ctx.client.delete(`/api/v1/issues/${issue}/links/${str3(link["id"])}`);
1728
- return { json: { deleted: link["id"] }, human: `Removed ${clean(link["url"])} from ${issue}
1729
- ` };
1730
- }
1731
- async function labelList(ctx) {
1732
- const body = await ctx.client.get("/api/v1/labels");
1733
- const human = body.data.length === 0 ? "No labels.\n" : table(
1734
- ["NAME", "COLOR", "ISSUES", "ID"],
1735
- body.data.map((label) => [
1736
- str3(label["name"]),
1737
- str3(label["color"]) || "-",
1738
- str3(label["issue_count"] ?? 0),
1739
- str3(label["id"])
1740
- ])
1741
- );
1742
- return { json: body.data, human };
1743
- }
1744
- async function labelCreate(ctx, name, color) {
1745
- if (name === void 0) throw new UsageError("usage: harness label create <name> [--color <color>]");
1746
- let body = { name };
1747
- body = putOpt2(body, "color", color);
1748
- const response = await ctx.client.post("/api/v1/labels", body);
1749
- return { json: response.data, human: `Created label ${clean(response.data["name"])}
1750
- ` };
1751
- }
1752
- var TEAM_CREATE_KEY = /^[A-Za-z][A-Za-z0-9]*$/;
1753
- async function teamCreate(ctx, opts) {
1754
- if (opts.key === void 0 || opts.name === void 0) {
1755
- throw new UsageError("usage: harness team create --key <key> --name <name>");
1756
- }
1757
- if (!TEAM_CREATE_KEY.test(opts.key)) {
1758
- throw new UsageError(`not a team key: ${opts.key} (expected e.g. ALL \u2014 letters and digits, letter first)`);
1759
- }
1760
- const created = await ctx.client.post("/api/v1/teams", { key: opts.key, name: opts.name });
1761
- const team = created.data;
1762
- const states = await ctx.client.get(`/api/v1/teams/${str3(team["key"])}/workflow_states`);
1763
- const human = `Created team ${str3(team["key"])} \u2014 ${clean(team["name"])}
1764
- ` + table(
1765
- ["NAME", "TYPE"],
1766
- states.data.map((state) => [str3(state["name"]), str3(state["type"])])
1767
- );
1768
- return { json: { ...team, workflow_states: states.data }, human };
1769
- }
1770
- async function teamList(ctx) {
1771
- const body = await ctx.client.get("/api/v1/teams");
1772
- const human = body.data.length === 0 ? "No teams.\n" : table(
1773
- ["KEY", "NAME", "ISSUES", "DEFAULT"],
1774
- body.data.map((team) => [
1775
- str3(team["key"]),
1776
- str3(team["name"]),
1777
- str3(team["issue_count"] ?? 0),
1778
- team["is_default"] === true ? "yes" : ""
1779
- ])
1780
- );
1781
- return { json: body.data, human };
1782
- }
1783
- async function workflowStates(ctx, team) {
1784
- const key = requiredTeam({ client: ctx.client, config: ctx.config }, team);
1785
- const body = await ctx.client.get(`/api/v1/teams/${key}/workflow_states`);
1786
- const human = body.data.length === 0 ? "No workflow states.\n" : table(
1787
- ["NAME", "TYPE", "COLOR", "ID"],
1788
- body.data.map((state) => [
1789
- str3(state["name"]),
1790
- str3(state["type"]),
1791
- str3(state["color"]) || "-",
1792
- str3(state["id"])
1793
- ])
1794
- );
1795
- return { json: body.data, human };
1796
- }
1797
- async function userList(ctx) {
1798
- const body = await ctx.client.get("/api/v1/users");
1799
- const human = body.data.length === 0 ? "No users.\n" : table(
1800
- ["NAME", "EMAIL", "ID"],
1801
- body.data.map((user) => [str3(user["name"]), str3(user["email"]), str3(user["id"])])
1802
- );
1803
- return { json: body.data, human };
1804
- }
1805
- async function teamRename(ctx, key, name) {
1806
- if (key === void 0 || name === void 0) {
1807
- throw new UsageError("usage: harness team rename <key> --name <name>");
1808
- }
1809
- const body = await ctx.client.patch(`/api/v1/teams/${encodeURIComponent(key)}`, { name });
1810
- return { json: body.data, human: `Renamed ${str3(body.data["key"])} to ${clean(body.data["name"])}
1811
- ` };
1812
- }
1813
- async function teamSetDefault(ctx, key) {
1814
- if (key === void 0) throw new UsageError("usage: harness team set-default <key>");
1815
- const body = await ctx.client.put(`/api/v1/teams/${encodeURIComponent(key)}/default`, {});
1816
- return {
1817
- json: body.data,
1818
- human: `${str3(body.data["key"])} is now the default filing target
1819
- `
1820
- };
1821
- }
1822
- async function stateAdd(ctx, opts) {
1823
- const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1824
- if (opts.name === void 0 || opts.type === void 0) {
1825
- throw new UsageError("usage: harness state add --name <name> --type <type> [--color <color>] [--team <key>]");
1826
- }
1827
- let payload = { name: opts.name, type: opts.type };
1828
- payload = putOpt2(payload, "color", opts.color);
1829
- const body = await ctx.client.post(`/api/v1/teams/${key}/workflow_states`, payload);
1830
- return { json: body.data, human: `Added ${clean(body.data["name"])} to ${key}
1831
- ` };
1832
- }
1833
- async function stateUpdate(ctx, id, opts) {
1834
- const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1835
- if (id === void 0) throw new UsageError("usage: harness state update <id> [--name <name>] [--type <type>] [--color <color>]");
1836
- if (opts.name === void 0 && opts.type === void 0 && opts.color === void 0) {
1837
- throw new UsageError("nothing to change \u2014 pass at least one of --name, --type, --color");
1838
- }
1839
- let payload = {};
1840
- payload = putOpt2(payload, "name", opts.name);
1841
- payload = putOpt2(payload, "type", opts.type);
1842
- payload = putOpt2(payload, "color", opts.color);
1843
- const body = await ctx.client.patch(`/api/v1/teams/${key}/workflow_states/${encodeURIComponent(id)}`, payload);
1844
- return { json: body.data, human: `Updated ${clean(body.data["name"])}
1845
- ` };
1846
- }
1847
- async function stateOrder(ctx, ids, opts) {
1848
- const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1849
- if (ids === void 0) {
1850
- throw new UsageError("usage: harness state order <id,id,id> [--team <key>] \u2014 every state id, in the order you want");
1851
- }
1852
- const state_ids = ids.split(",").map((id) => id.trim()).filter((id) => id !== "");
1853
- if (state_ids.length === 0) throw new UsageError("no state ids given");
1854
- const body = await ctx.client.put(`/api/v1/teams/${key}/workflow_states/order`, { state_ids });
1855
- const human = table(
1856
- ["NAME", "TYPE"],
1857
- body.data.map((state) => [str3(state["name"]), str3(state["type"])])
1858
- );
1859
- return { json: body.data, human };
1860
- }
1861
- async function stateDelete(ctx, id, opts) {
1862
- const key = requiredTeam({ client: ctx.client, config: ctx.config }, opts.team);
1863
- if (id === void 0) throw new UsageError("usage: harness state delete <id> --yes [--team <key>]");
1864
- requireYes("state delete", id, opts.yes);
1865
- await ctx.client.delete(`/api/v1/teams/${key}/workflow_states/${encodeURIComponent(id)}`);
1866
- return { json: { deleted: id }, human: `Deleted workflow state ${id}
1867
- ` };
1868
- }
1869
- async function labelUpdate(ctx, id, opts) {
1870
- if (id === void 0) throw new UsageError("usage: harness label update <id> [--name <name>] [--color <color>]");
1871
- if (opts.name === void 0 && opts.color === void 0) {
1872
- throw new UsageError("nothing to change \u2014 pass --name, --color, or both");
1873
- }
1874
- let payload = {};
1875
- payload = putOpt2(payload, "name", opts.name);
1876
- payload = putOpt2(payload, "color", opts.color);
1877
- const body = await ctx.client.patch(`/api/v1/labels/${encodeURIComponent(id)}`, payload);
1878
- return { json: body.data, human: `Updated label ${clean(body.data["name"])}
1879
- ` };
1880
- }
1881
- async function labelDelete(ctx, id, opts) {
1882
- if (id === void 0) throw new UsageError("usage: harness label delete <id> --yes");
1883
- requireYes("label delete", id, opts.yes);
1884
- await ctx.client.delete(`/api/v1/labels/${encodeURIComponent(id)}`);
1885
- return { json: { deleted: id }, human: `Deleted label ${id}
1886
- ` };
1887
- }
1888
- async function memberList(ctx) {
1889
- const body = await ctx.client.get("/api/v1/members");
1890
- const human = body.data.length === 0 ? "No members.\n" : table(
1891
- ["NAME", "EMAIL", "RANK", "STANDING", "ID"],
1892
- body.data.map((member) => [
1893
- str3(field(member, "user", "name")),
1894
- str3(field(member, "user", "email")),
1895
- str3(member["role"]) || "-",
1896
- str3(member["standing"]),
1897
- str3(field(member, "user", "id"))
1898
- ])
1899
- );
1900
- return { json: body.data, human };
1901
- }
1902
- async function memberAdd(ctx, opts) {
1903
- if (opts.email === void 0 || opts.role === void 0) {
1904
- throw new UsageError("usage: harness member add --email <email> --role <admin|staff|collaborator>");
1905
- }
1906
- const body = await ctx.client.post("/api/v1/members", { email: opts.email, role: opts.role });
1907
- return {
1908
- json: body.data,
1909
- human: `Added ${str3(field(body.data, "user", "email"))} as ${str3(body.data["role"])}
1910
- `
1911
- };
1912
- }
1913
- async function memberSetRole(ctx, userId, opts) {
1914
- if (userId === void 0 || opts.role === void 0) {
1915
- throw new UsageError("usage: harness member set-role <user-id> --role <admin|staff|collaborator|none>");
1916
- }
1917
- const body = await ctx.client.put(`/api/v1/members/${encodeURIComponent(userId)}/role`, { role: opts.role });
1918
- const rank = str3(body.data["role"]);
1919
- const human = rank === "" ? `Revoked the rank for ${str3(field(body.data, "user", "email"))}
1920
- ` : `${str3(field(body.data, "user", "email"))} is now ${rank}
1921
- `;
1922
- return { json: body.data, human };
1923
- }
1924
- function requireYes(command, id, yes) {
1925
- if (yes !== true) throw new UsageError(`${command} ${id} is destructive \u2014 pass --yes to confirm`);
1926
- }
1927
- async function upload(ctx, path2) {
1928
- return attachFile(ctx, path2);
1929
- }
1930
- function indent2(text) {
1931
- return cleanMultiline(text).split("\n").map((line) => " " + line).join("\n");
1932
- }
1933
-
1934
- // src/core/credentials.ts
1935
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1936
- import { homedir } from "os";
1937
- import { dirname as dirname2, isAbsolute, join as join2 } from "path";
1938
- function defaultCredentialsPath(env = process.env) {
1939
- const xdg = env["XDG_CONFIG_HOME"];
1940
- const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
1941
- return join2(base, "harness", "credentials.json");
1942
- }
1943
- function readCredentials(path2) {
1944
- try {
1945
- const parsed = JSON.parse(readFileSync3(path2, "utf8"));
1946
- if (typeof parsed === "object" && parsed !== null) {
1947
- const { url, token } = parsed;
1948
- if (typeof url === "string" && typeof token === "string") return { url, token };
1949
- }
1950
- } catch {
1951
- }
1952
- return null;
1953
- }
1954
- var uniqueCounter = 0;
1955
- function writeCredentials(path2, url, token) {
1956
- mkdirSync2(dirname2(path2), { recursive: true });
1957
- const tempPath = `${path2}.${process.pid}.${++uniqueCounter}.tmp`;
1958
- try {
1959
- writeFileSync2(tempPath, "", { mode: 384 });
1960
- chmodSync2(tempPath, 384);
1961
- writeFileSync2(tempPath, JSON.stringify({ url, token }));
1962
- renameSync2(tempPath, path2);
1963
- } finally {
1964
- rmSync2(tempPath, { force: true });
1965
- }
1966
- }
1967
- function deleteCredentials(path2) {
1968
- rmSync2(path2, { force: true });
1969
- }
1970
-
1971
- // src/core/repo-config.ts
1972
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1973
- import { homedir as homedir2 } from "os";
1974
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, parse } from "path";
1975
- var CONFIG_RELATIVE_PATH = join3(".harness-cli", "config.json");
1976
- function findRepoConfig(startDir = process.cwd()) {
1977
- let dir = startDir;
1978
- const { root } = parse(startDir);
1979
- for (; ; ) {
1980
- const candidate = join3(dir, CONFIG_RELATIVE_PATH);
1981
- if (existsSync(candidate)) {
1982
- return readRepoConfig(candidate);
1983
- }
1984
- if (dir === root) return { config: null, warnings: [] };
1985
- dir = dirname3(dir);
1986
- }
1987
- }
1988
- function readRepoConfig(path2) {
1989
- let parsed;
1990
- try {
1991
- parsed = JSON.parse(readFileSync4(path2, "utf8"));
1992
- } catch (error) {
1993
- const detail = error instanceof Error ? error.message : String(error);
1994
- return { config: null, warnings: [`harness: ignoring malformed ${path2} (${detail})`] };
1995
- }
1996
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1997
- return { config: null, warnings: [`harness: ignoring ${path2} \u2014 expected a JSON object`] };
1998
- }
1999
- const record = parsed;
2000
- const config = { path: path2 };
2001
- const warnings = [];
2002
- for (const field2 of ["team", "url", "org"]) {
2003
- const value = record[field2];
2004
- if (value === void 0) continue;
2005
- if (typeof value === "string" && value.trim() !== "") {
2006
- config[field2] = value.trim();
2007
- } else {
2008
- warnings.push(`harness: ignoring "${field2}" in ${path2} \u2014 expected a non-empty string`);
2009
- }
2010
- }
2011
- return { config, warnings };
2012
- }
2013
- function trustedHostsPath(env = process.env) {
2014
- const xdg = env["XDG_CONFIG_HOME"];
2015
- const base = xdg !== void 0 && xdg !== "" && isAbsolute2(xdg) ? xdg : join3(homedir2(), ".config");
2016
- return join3(base, "harness", "trusted-hosts.json");
2017
- }
2018
- function readTrustedHosts(env) {
2019
- try {
2020
- const parsed = JSON.parse(readFileSync4(trustedHostsPath(env), "utf8"));
2021
- if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed["hosts"])) {
2022
- return parsed["hosts"].filter(
2023
- (host) => typeof host === "string"
2024
- );
2025
- }
2026
- } catch {
2027
- }
2028
- return [];
2029
- }
2030
- function isHostTrusted(baseUrl, env) {
2031
- return readTrustedHosts(env).includes(baseUrl);
2032
- }
2033
- function trustHost(baseUrl, env) {
2034
- const hosts = readTrustedHosts(env);
2035
- if (hosts.includes(baseUrl)) return;
2036
- const path2 = trustedHostsPath(env);
2037
- mkdirSync3(dirname3(path2), { recursive: true });
2038
- writeFileSync3(path2, JSON.stringify({ hosts: [...hosts, baseUrl] }, null, 2) + "\n");
2039
- }
2040
-
2041
- // src/core/config.ts
2042
- import { homedir as homedir3, tmpdir } from "os";
2043
- import { isAbsolute as isAbsolute3, join as join4 } from "path";
2044
- var DEFAULT_BASE_URL = "https://harness.infra.one";
2045
- function resolveConfig(env = process.env, credentialsPath, repo) {
2046
- let warnings = [];
2047
- if (repo === void 0) {
2048
- const lookup = findRepoConfig();
2049
- repo = lookup.config;
2050
- warnings = lookup.warnings;
2051
- }
2052
- const path2 = credentialsPath ?? defaultCredentialsPath(env);
2053
- const stored = readCredentials(path2);
2054
- const envUrl = blankToNull(env["HARNESS_API_URL"]);
2055
- const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
2056
- const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
2057
- let rawUrl;
2058
- let hostSource;
2059
- if (envUrl !== null) {
2060
- rawUrl = envUrl;
2061
- hostSource = "env";
2062
- } else if (envToken !== null) {
2063
- if (repoUrl !== null && normalize(repoUrl) !== normalize(DEFAULT_BASE_URL)) {
2064
- return {
2065
- kind: "error",
2066
- 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`,
2067
- warnings
2068
- };
2069
- }
2070
- rawUrl = DEFAULT_BASE_URL;
2071
- hostSource = "default";
2072
- } else if (repoUrl !== null) {
2073
- rawUrl = repoUrl;
2074
- hostSource = "repo-config";
2075
- } else if (stored !== null) {
2076
- rawUrl = stored.url;
2077
- hostSource = "stored-login";
2078
- } else {
2079
- rawUrl = DEFAULT_BASE_URL;
2080
- hostSource = "default";
2081
- }
2082
- const validated = validateBaseUrl(rawUrl);
2083
- if (!validated.ok) return { kind: "error", message: validated.message, warnings };
2084
- const baseUrl = rawUrl.replace(/\/+$/, "");
2085
- let token;
2086
- if (envToken !== null) {
2087
- token = envToken;
2088
- } else if (stored !== null && normalize(stored.url) !== null && normalize(stored.url) === normalize(baseUrl)) {
2089
- token = stored.token;
2090
- } else {
2091
- const result = { kind: "needs-login", baseUrl, hostSource, warnings };
2092
- if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
2093
- return result;
2094
- }
2095
- return {
2096
- kind: "ok",
2097
- config: {
2098
- baseUrl,
2099
- token,
2100
- actor: blankToNull(env["HARNESS_ACTOR"]),
2101
- // The committed team is the fallback: the environment (and, above it,
2102
- // the --team flag at the command layer) stays authoritative.
2103
- team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
2104
- cacheDir: resolveCacheDir(env),
2105
- hostSource
2106
- },
2107
- warnings
2108
- };
2109
- }
2110
- function resolveCacheDir(env = process.env) {
2111
- return absolute(env["HARNESS_CACHE_DIR"]) ?? join4(resolveCacheHome(env), "harness", "attachments");
2112
- }
2113
- function absolute(value) {
2114
- return value !== void 0 && value !== "" && isAbsolute3(value) ? value : null;
2115
- }
2116
- function resolveCacheHome(env = process.env) {
2117
- return absolute(env["XDG_CACHE_HOME"]) ?? join4(homedir3() || tmpdir(), ".cache");
2118
- }
2119
- function normalize(url) {
2120
- let parsed;
2121
- try {
2122
- parsed = new URL(url);
2123
- } catch {
2124
- return null;
2125
- }
2126
- const port = parsed.port !== "" ? parsed.port : parsed.protocol === "https:" ? "443" : parsed.protocol === "http:" ? "80" : "";
2127
- return JSON.stringify([parsed.protocol, parsed.hostname, port, parsed.pathname.replace(/\/+$/, "")]);
2128
- }
2129
- function validateBaseUrl(baseUrl) {
2130
- let parsed;
2131
- try {
2132
- parsed = new URL(baseUrl);
2133
- } catch {
2134
- return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
2135
- }
2136
- if (parsed.username !== "" || parsed.password !== "") {
2137
- return { ok: false, message: "HARNESS_API_URL must not embed credentials (user:pass@) \u2014 use HARNESS_API_TOKEN" };
2138
- }
2139
- if (parsed.search !== "" || parsed.hash !== "") {
2140
- return { ok: false, message: `HARNESS_API_URL must not carry a query or fragment, got: ${baseUrl}` };
2141
- }
2142
- if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
2143
- if (parsed.protocol === "http:") {
2144
- if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) return { ok: true };
2145
- return {
2146
- ok: false,
2147
- message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost only.`
2148
- };
2149
- }
2150
- return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
2151
- }
2152
- function loginHelp(baseUrl) {
2153
- return `Not signed in to ${baseUrl}.
2154
-
2155
- Interactively, in a terminal (not from inside an agent \u2014 the browser
2156
- tab needs someone to see it):
2157
-
2158
- harness login
2159
-
2160
- For agents and CI, mint an API token (admin \u2192 API tokens) with the
2161
- harness:read scope \u2014 plus harness:write only where the workflow
2162
- creates or closes \u2014 set an expiry, and export it:
2163
-
2164
- export HARNESS_API_TOKEN=aone_...
2165
- `;
2166
- }
2167
- function blankToNull(value) {
2168
- if (value === void 0 || value.trim() === "") return null;
2169
- return value;
2170
- }
2171
-
2172
- export {
2173
- HttpError,
2174
- UpgradeRequiredError,
2175
- TransportError,
2176
- UsageError,
2177
- LocalError,
2178
- CommandError,
2179
- CLI_VERSION,
2180
- Client,
2181
- clean,
2182
- json,
2183
- MAX_IDEMPOTENCY_KEY_BYTES,
2184
- IDEMPOTENCY_KEY_FORMAT,
2185
- requireIdentifiers,
2186
- ready,
2187
- blocked,
2188
- list,
2189
- search,
2190
- show,
2191
- ACTIVITY_MAX_LIMIT,
2192
- activity,
2193
- stats2 as stats,
2194
- prime,
2195
- version,
2196
- create,
2197
- update,
2198
- requireCloseArgs,
2199
- close,
2200
- archive,
2201
- comments,
2202
- dep,
2203
- RELATION_CREATE_TYPES,
2204
- RELATION_UPDATE_TYPES,
2205
- relationList,
2206
- relationAdd,
2207
- relationUpdate,
2208
- relationRm,
2209
- comment,
2210
- editComment,
2211
- deleteComment,
2212
- requireCommentId,
2213
- attachImage,
2214
- projectList,
2215
- projectShow,
2216
- projectCreate,
2217
- projectUpdate,
2218
- milestoneList,
2219
- milestoneCreate,
2220
- docList,
2221
- docShow,
2222
- docCreate,
2223
- docUpdate,
2224
- linkList,
2225
- linkAdd,
2226
- linkUpdate,
2227
- linkRm,
2228
- labelList,
2229
- labelCreate,
2230
- teamCreate,
2231
- teamList,
2232
- workflowStates,
2233
- userList,
2234
- teamRename,
2235
- teamSetDefault,
2236
- stateAdd,
2237
- stateUpdate,
2238
- stateOrder,
2239
- stateDelete,
2240
- labelUpdate,
2241
- labelDelete,
2242
- memberList,
2243
- memberAdd,
2244
- memberSetRole,
2245
- upload,
2246
- defaultCredentialsPath,
2247
- readCredentials,
2248
- writeCredentials,
2249
- deleteCredentials,
2250
- findRepoConfig,
2251
- isHostTrusted,
2252
- trustHost,
2253
- DEFAULT_BASE_URL,
2254
- resolveConfig,
2255
- resolveCacheHome,
2256
- validateBaseUrl,
2257
- loginHelp
2258
- };