@allocator-one/harness-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/chunk-KN5ILZSA.js +1459 -0
- package/dist/cli.js +718 -0
- package/dist/server-PI2AZOVY.js +434 -0
- package/package.json +44 -0
- package/spec/openapi.json +2099 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Client,
|
|
4
|
+
CommandError,
|
|
5
|
+
DEFAULT_BASE_URL,
|
|
6
|
+
HttpError,
|
|
7
|
+
LocalError,
|
|
8
|
+
TransportError,
|
|
9
|
+
UpgradeRequiredError,
|
|
10
|
+
UsageError,
|
|
11
|
+
archive,
|
|
12
|
+
attachImage,
|
|
13
|
+
blocked,
|
|
14
|
+
clean,
|
|
15
|
+
close,
|
|
16
|
+
comment,
|
|
17
|
+
comments,
|
|
18
|
+
create,
|
|
19
|
+
defaultCredentialsPath,
|
|
20
|
+
deleteCredentials,
|
|
21
|
+
dep,
|
|
22
|
+
docCreate,
|
|
23
|
+
docList,
|
|
24
|
+
docShow,
|
|
25
|
+
docUpdate,
|
|
26
|
+
json,
|
|
27
|
+
labelCreate,
|
|
28
|
+
labelList,
|
|
29
|
+
linkAdd,
|
|
30
|
+
linkList,
|
|
31
|
+
linkRm,
|
|
32
|
+
list,
|
|
33
|
+
loginHelp,
|
|
34
|
+
milestoneCreate,
|
|
35
|
+
milestoneList,
|
|
36
|
+
prime,
|
|
37
|
+
projectCreate,
|
|
38
|
+
projectList,
|
|
39
|
+
projectShow,
|
|
40
|
+
projectUpdate,
|
|
41
|
+
readCredentials,
|
|
42
|
+
ready,
|
|
43
|
+
requireIdentifiers,
|
|
44
|
+
resolveConfig,
|
|
45
|
+
search,
|
|
46
|
+
show,
|
|
47
|
+
stats,
|
|
48
|
+
teamList,
|
|
49
|
+
update,
|
|
50
|
+
upload,
|
|
51
|
+
userList,
|
|
52
|
+
validateBaseUrl,
|
|
53
|
+
version,
|
|
54
|
+
workflowStates,
|
|
55
|
+
writeCredentials
|
|
56
|
+
} from "./chunk-KN5ILZSA.js";
|
|
57
|
+
|
|
58
|
+
// src/cli.ts
|
|
59
|
+
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
60
|
+
|
|
61
|
+
// src/core/auth.ts
|
|
62
|
+
import { createHash, randomBytes } from "crypto";
|
|
63
|
+
import { execFile } from "child_process";
|
|
64
|
+
import { accessSync, constants as fsConstants } from "fs";
|
|
65
|
+
import { createServer } from "http";
|
|
66
|
+
import { delimiter, join } from "path";
|
|
67
|
+
import { createInterface } from "readline";
|
|
68
|
+
|
|
69
|
+
// src/core/login-page.ts
|
|
70
|
+
var LOGO = '<svg width="210" height="34" viewBox="0 0 210 34" fill="none" role="img" aria-label="Infra One" xmlns="http://www.w3.org/2000/svg">\n <style>\n path {\n fill: #353535\n }\n\n @media (prefers-color-scheme: dark) {\n path {\n fill: white\n }\n }\n </style>\n <g id="I1_logo_full">\n <g id="Infra One Logo">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M44.2202 29.5139V4.04761H49.617V29.5139H44.2202Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M121.159 11.873V13.998C119.877 12.4464 117.853 11.4345 115.29 11.4345C110.23 11.4345 106.284 15.3809 106.284 20.7103C106.284 26.0397 110.23 29.9861 115.29 29.9861C118.157 29.9861 119.877 28.9742 121.159 27.3214V29.4464H125.948V11.9067H121.159V11.873ZM116.099 25.8373C113.063 25.8373 110.938 23.7123 110.938 20.6091C110.938 17.5059 113.063 15.4484 116.099 15.4484C119.135 15.4484 121.327 17.5734 121.327 20.6091C121.327 23.6448 119.034 25.8373 116.099 25.8373Z" />\n <path d="M15.6508 0H27.8611V34H15.6508V0ZM0 11.131C0 7.92659 2.66468 5.2619 5.86905 5.2619H6.34127C9.54564 5.2619 12.2103 7.92659 12.2103 11.131C12.2103 14.3353 9.54564 17 6.34127 17H5.86905C2.66468 17 0 14.3353 0 11.131Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M88.4405 10.4901C87.6984 10.2203 87.3611 10.0179 86.3155 10.0179C84.8651 10.0179 84.123 10.7599 84.123 12.2103V13.627H89.4524V17.6746H84.123V29.4802H79.3333V17.6746H76.2976V13.627H79.3333V12.2103C79.3333 8.36511 81.1548 5.86908 85.4048 5.86908C87.631 5.86908 88.1706 6.13892 89.621 6.71233L88.4405 10.4901Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M98.0199 14.4365C99.133 12.4127 101.224 11.2321 104.091 11.6032V15.9206C100.246 15.381 98.0199 17.0337 98.0199 21.7897V29.4127H93.2302V11.873H98.0199V14.4365Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M65.5715 11.4008C70.3611 11.4008 72.756 14.5377 72.756 19.2262V29.5139H67.9663V20.0357C67.9663 17.371 66.6845 15.6171 64.1211 15.6171C61.5576 15.6171 59.7024 17.6409 59.7024 20.7778V29.5139H54.9127V11.873H59.7024V14.0655C60.9842 12.5139 62.9068 11.4008 65.5715 11.4008Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M151.01 4.621C143.387 4.621 137.687 10.0516 137.687 17.3035C137.687 24.5555 143.286 29.9861 151.01 29.9861C158.734 29.9861 164.232 24.4881 164.232 17.3035C164.232 10.119 158.633 4.621 151.01 4.621ZM151.01 25.5674C145.95 25.5674 142.746 21.992 142.746 17.3035C142.746 12.6151 145.95 9.10712 151.01 9.10712C156.069 9.10712 159.274 12.7837 159.274 17.3035C159.274 21.8234 155.968 25.5674 151.01 25.5674Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M179.309 11.4008C184.099 11.4008 186.494 14.5377 186.494 19.2262V29.5139H181.704V20.0357C181.704 17.371 180.423 15.6171 177.859 15.6171C175.296 15.6171 173.44 17.6409 173.44 20.7778V29.5139H168.651V11.873H173.44V14.0655C174.722 12.5139 176.645 11.4008 179.309 11.4008Z" />\n <path fill-rule="evenodd" clip-rule="evenodd" d="M209.565 20.2381C209.565 15.1786 206.26 11.3333 200.188 11.3333C194.117 11.3333 190.643 15.3809 190.643 20.6091C190.643 25.8373 194.69 29.8849 200.559 29.8849C206.429 29.8849 208.284 27.119 209.397 24.8254L205.079 23.6448C204.337 24.9266 202.954 25.9385 200.661 25.9385C197.355 25.9385 195.702 24.1845 195.331 21.8909H209.498C209.599 21.3512 209.599 20.7778 209.599 20.2381H209.565ZM195.298 19.125C195.567 17.1012 197.321 15.2797 200.155 15.2797C202.988 15.2797 204.742 17.0337 204.944 19.125H195.298Z" />\n </g>\n </g>\n</svg>';
|
|
71
|
+
function escapeHtml(text) {
|
|
72
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
73
|
+
}
|
|
74
|
+
function loginPage(message) {
|
|
75
|
+
return `<!doctype html>
|
|
76
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
77
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
78
|
+
<!-- The logo paints itself white under a dark preference; without this the
|
|
79
|
+
canvas stays white and the logo disappears. -->
|
|
80
|
+
<meta name="color-scheme" content="light dark">
|
|
81
|
+
<title>harness login</title></head>
|
|
82
|
+
<body style="margin: 0;">
|
|
83
|
+
<main style="min-height: 100vh; padding: 1.5rem; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; text-align: center; font-family: system-ui, sans-serif;">
|
|
84
|
+
${LOGO}
|
|
85
|
+
<p style="margin: 0;">${escapeHtml(message)}</p>
|
|
86
|
+
</main>
|
|
87
|
+
</body></html>
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/core/auth.ts
|
|
92
|
+
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
93
|
+
function generateState() {
|
|
94
|
+
return randomBytes(24).toString("base64url");
|
|
95
|
+
}
|
|
96
|
+
function generateVerifier() {
|
|
97
|
+
return randomBytes(32).toString("base64url");
|
|
98
|
+
}
|
|
99
|
+
function challengeFor(verifier) {
|
|
100
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
101
|
+
}
|
|
102
|
+
async function login(baseUrl, options = {}) {
|
|
103
|
+
const validated = validateBaseUrl(baseUrl);
|
|
104
|
+
if (!validated.ok) return { ok: false, message: validated.message };
|
|
105
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
106
|
+
if (options.noBrowser === true || !browserAvailable(options)) {
|
|
107
|
+
return manualLogin(base, options);
|
|
108
|
+
}
|
|
109
|
+
return loopbackLogin(base, options);
|
|
110
|
+
}
|
|
111
|
+
function browserAvailable(options) {
|
|
112
|
+
if ("openBrowser" in options) return typeof options.openBrowser === "function";
|
|
113
|
+
return browserOpener() !== null && displayAvailable(options.env ?? process.env);
|
|
114
|
+
}
|
|
115
|
+
function findExecutable(name, pathVar = process.env["PATH"]) {
|
|
116
|
+
for (const dir of (pathVar ?? "").split(delimiter)) {
|
|
117
|
+
if (dir === "") continue;
|
|
118
|
+
const candidate = join(dir, name);
|
|
119
|
+
try {
|
|
120
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
121
|
+
return candidate;
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function browserOpener() {
|
|
128
|
+
const name = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
|
|
129
|
+
if (name === null) return null;
|
|
130
|
+
return findExecutable(name);
|
|
131
|
+
}
|
|
132
|
+
function displayAvailable(env) {
|
|
133
|
+
const display = env["DISPLAY"];
|
|
134
|
+
const remote = isSet(env["SSH_CONNECTION"]) || isSet(env["SSH_TTY"]);
|
|
135
|
+
const hasDisplay = isSet(display) || isSet(env["WAYLAND_DISPLAY"]);
|
|
136
|
+
const forwarded = typeof display === "string" && display.startsWith("localhost:");
|
|
137
|
+
if (process.platform === "darwin") return !remote;
|
|
138
|
+
if (process.platform === "linux") return hasDisplay && (!remote || forwarded);
|
|
139
|
+
return !remote;
|
|
140
|
+
}
|
|
141
|
+
function isSet(value) {
|
|
142
|
+
return value !== void 0 && value !== "";
|
|
143
|
+
}
|
|
144
|
+
async function loopbackLogin(baseUrl, options) {
|
|
145
|
+
const notify = options.notify ?? defaultNotify;
|
|
146
|
+
const state = generateState();
|
|
147
|
+
const verifier = generateVerifier();
|
|
148
|
+
const challenge = challengeFor(verifier);
|
|
149
|
+
const server = createServer();
|
|
150
|
+
const port = await new Promise((resolve) => {
|
|
151
|
+
server.once("error", () => resolve(null));
|
|
152
|
+
server.listen(0, "127.0.0.1", () => {
|
|
153
|
+
const address = server.address();
|
|
154
|
+
resolve(typeof address === "object" && address !== null ? address.port : null);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
if (port === null) {
|
|
158
|
+
return { ok: false, message: "could not open a loopback port for the browser handoff" };
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const query = new URLSearchParams({ port: String(port), state, challenge });
|
|
162
|
+
const url = `${baseUrl}/cli/login?${query.toString()}`;
|
|
163
|
+
const openBrowser = options.openBrowser ?? defaultOpenBrowser;
|
|
164
|
+
openBrowser(url);
|
|
165
|
+
notify(`Opening ${url}`);
|
|
166
|
+
notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
|
|
167
|
+
const code = await awaitCallback(server, state, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
168
|
+
if (!code.ok) return code;
|
|
169
|
+
const token = await exchangeCode(baseUrl, code.code, verifier, options);
|
|
170
|
+
if (!token.ok) return token;
|
|
171
|
+
return await verifyAndStore(baseUrl, token.token, options);
|
|
172
|
+
} finally {
|
|
173
|
+
server.closeAllConnections();
|
|
174
|
+
server.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function awaitCallback(server, state, timeoutMs) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const timer = setTimeout(() => {
|
|
180
|
+
resolve({ ok: false, message: "timed out waiting for the browser authorization" });
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
server.on("request", (request, response) => {
|
|
183
|
+
const respond = (status, message) => {
|
|
184
|
+
response.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
185
|
+
response.end(loginPage(message));
|
|
186
|
+
};
|
|
187
|
+
let url;
|
|
188
|
+
try {
|
|
189
|
+
url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
190
|
+
} catch {
|
|
191
|
+
respond(404, "Waiting for the harness login callback.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (url.pathname !== "/callback") {
|
|
195
|
+
respond(404, "Waiting for the harness login callback.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const code = url.searchParams.get("code");
|
|
199
|
+
if (url.searchParams.get("state") === state && code !== null && code !== "") {
|
|
200
|
+
respond(200, "Signed in \u2014 you can close this tab and return to the terminal.");
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
resolve({ ok: true, code });
|
|
203
|
+
} else {
|
|
204
|
+
respond(400, "This is not the callback this login is waiting for \u2014 run harness login again if needed.");
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function exchangeCode(baseUrl, code, verifier, options) {
|
|
210
|
+
try {
|
|
211
|
+
const body = await bareClient(baseUrl, "", options).post(
|
|
212
|
+
"/api/v1/cli/token-exchange",
|
|
213
|
+
{ code, code_verifier: verifier }
|
|
214
|
+
);
|
|
215
|
+
const token = body.data?.token;
|
|
216
|
+
if (typeof token === "string" && token !== "") return { ok: true, token };
|
|
217
|
+
return { ok: false, message: "the login-code exchange returned no token \u2014 aborting" };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error instanceof HttpError) {
|
|
220
|
+
return { ok: false, message: `the login-code exchange failed: ${error.message}` };
|
|
221
|
+
}
|
|
222
|
+
if (error instanceof TransportError) {
|
|
223
|
+
return { ok: false, message: `the login-code exchange failed: cannot reach the API (${error.message})` };
|
|
224
|
+
}
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function manualLogin(baseUrl, options) {
|
|
229
|
+
const notify = options.notify ?? defaultNotify;
|
|
230
|
+
const prompt = options.prompt ?? defaultPrompt;
|
|
231
|
+
notify(`Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below.`);
|
|
232
|
+
const token = (await prompt()).trim();
|
|
233
|
+
if (token === "") return { ok: false, message: "no token entered" };
|
|
234
|
+
return verifyAndStore(baseUrl, token, options);
|
|
235
|
+
}
|
|
236
|
+
async function verifyAndStore(baseUrl, token, options) {
|
|
237
|
+
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
238
|
+
const client = bareClient(baseUrl, token, options);
|
|
239
|
+
try {
|
|
240
|
+
await client.get("/api/v1/teams");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof HttpError) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (error instanceof TransportError) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
message: `could not verify the token: cannot reach the API (${error.message}) \u2014 nothing was stored`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
const stored = readCredentials(path);
|
|
257
|
+
const superseded = stored !== null && stored.token !== token ? stored : null;
|
|
258
|
+
try {
|
|
259
|
+
writeCredentials(path, baseUrl, token);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
await client.delete("/api/v1/session").catch(() => void 0);
|
|
262
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
263
|
+
return { ok: false, message: `could not store credentials at ${path}: ${detail}` };
|
|
264
|
+
}
|
|
265
|
+
if (superseded !== null) {
|
|
266
|
+
await bareClient(superseded.url, superseded.token, options).delete("/api/v1/session").catch(() => void 0);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
json: { url: baseUrl, credentials: path },
|
|
271
|
+
human: `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; harness logout revokes it).
|
|
272
|
+
`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async function logout(options = {}) {
|
|
276
|
+
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
277
|
+
const stored = readCredentials(path);
|
|
278
|
+
if (stored === null) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
message: "No stored login to remove. A HARNESS_API_TOKEN from the environment is yours to manage \u2014 unset it, or disable the token in the admin UI."
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const revoked = await bareClient(stored.url, stored.token, options).delete("/api/v1/session").then(() => true).catch(() => false);
|
|
285
|
+
const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
|
|
286
|
+
try {
|
|
287
|
+
deleteCredentials(path);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
message: `could not remove ${path} (${detail}) \u2014 delete it manually.` + (revoked ? " The token itself was revoked." : "")
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
json: { logged_out: true, revoked },
|
|
298
|
+
human: `Logged out of ${stored.url}${note}.
|
|
299
|
+
`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function whoami(config, credentialSource) {
|
|
303
|
+
const client = new Client(config);
|
|
304
|
+
try {
|
|
305
|
+
await client.get("/api/v1/stats");
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof HttpError) {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
message: `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
const serverVersion = await client.serverVersion().catch(() => "unknown");
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
json: { url: config.baseUrl, source: credentialSource, verified: true, server_version: serverVersion },
|
|
319
|
+
human: `Host: ${config.baseUrl}
|
|
320
|
+
Credential: ${credentialSource}
|
|
321
|
+
Verified: ok (server API ${serverVersion})
|
|
322
|
+
`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function bareClient(baseUrl, token, options) {
|
|
326
|
+
const config = {
|
|
327
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
328
|
+
token,
|
|
329
|
+
actor: null,
|
|
330
|
+
team: null,
|
|
331
|
+
cacheDir: ""
|
|
332
|
+
};
|
|
333
|
+
if (options.fetchImpl !== void 0) config.fetchImpl = options.fetchImpl;
|
|
334
|
+
return new Client(config);
|
|
335
|
+
}
|
|
336
|
+
function defaultNotify(line) {
|
|
337
|
+
process.stderr.write(line + "\n");
|
|
338
|
+
}
|
|
339
|
+
function defaultPrompt() {
|
|
340
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
readline.question("Token: ", (answer) => {
|
|
343
|
+
readline.close();
|
|
344
|
+
resolve(answer);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function defaultOpenBrowser(url) {
|
|
349
|
+
const opener = browserOpener();
|
|
350
|
+
if (opener !== null) {
|
|
351
|
+
execFile(opener, [url], () => void 0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/cli.ts
|
|
356
|
+
function exitWith(code) {
|
|
357
|
+
const drain = (stream) => new Promise((resolve) => stream.write("", () => resolve()));
|
|
358
|
+
return Promise.all([drain(process.stdout), drain(process.stderr)]).then(() => process.exit(code));
|
|
359
|
+
}
|
|
360
|
+
var program = new Command();
|
|
361
|
+
program.name("harness").description("issue tracking for humans and agents (harness.infra.one)").exitOverride().configureOutput({
|
|
362
|
+
writeOut: (text) => process.stdout.write(text),
|
|
363
|
+
writeErr: (text) => process.stderr.write(text)
|
|
364
|
+
});
|
|
365
|
+
function integer(value) {
|
|
366
|
+
if (!/^\d+$/.test(value)) throw new InvalidArgumentError("expected a non-negative integer");
|
|
367
|
+
return Number.parseInt(value, 10);
|
|
368
|
+
}
|
|
369
|
+
function collect(value, previous) {
|
|
370
|
+
return [...previous, value];
|
|
371
|
+
}
|
|
372
|
+
var requireIdentifiers2 = requireIdentifiers;
|
|
373
|
+
function interactive() {
|
|
374
|
+
const ci = process.env["CI"];
|
|
375
|
+
return process.stdout.isTTY === true && (ci === void 0 || ci === "");
|
|
376
|
+
}
|
|
377
|
+
async function withConfig(jsonFlag, mutating, run) {
|
|
378
|
+
const resolved = resolveConfig();
|
|
379
|
+
if (resolved.kind === "error") {
|
|
380
|
+
process.stderr.write(`harness: ${resolved.message}
|
|
381
|
+
`);
|
|
382
|
+
return exitWith(2);
|
|
383
|
+
}
|
|
384
|
+
if (resolved.kind === "needs-login") {
|
|
385
|
+
if (!interactive()) {
|
|
386
|
+
process.stderr.write(loginHelp(resolved.baseUrl));
|
|
387
|
+
return exitWith(2);
|
|
388
|
+
}
|
|
389
|
+
const loginResult = await login(resolved.baseUrl, {});
|
|
390
|
+
if (!loginResult.ok) {
|
|
391
|
+
process.stderr.write(`harness: ${clean(loginResult.message)}
|
|
392
|
+
`);
|
|
393
|
+
return exitWith(1);
|
|
394
|
+
}
|
|
395
|
+
process.stderr.write(loginResult.human);
|
|
396
|
+
const again = resolveConfig();
|
|
397
|
+
if (again.kind !== "ok") {
|
|
398
|
+
process.stderr.write(`harness: still not signed in to ${resolved.baseUrl} after logging in
|
|
399
|
+
`);
|
|
400
|
+
return exitWith(1);
|
|
401
|
+
}
|
|
402
|
+
return execute(again.config, jsonFlag, mutating, run);
|
|
403
|
+
}
|
|
404
|
+
return execute(resolved.config, jsonFlag, mutating, run);
|
|
405
|
+
}
|
|
406
|
+
async function execute(config, jsonFlag, mutating, run) {
|
|
407
|
+
const client = new Client(config);
|
|
408
|
+
const hostNotice = mutating ? `\u2192 ${config.baseUrl}
|
|
409
|
+
` : "";
|
|
410
|
+
try {
|
|
411
|
+
const output = await run({ client, config });
|
|
412
|
+
process.stderr.write(hostNotice);
|
|
413
|
+
if (output.notice !== void 0) process.stderr.write(output.notice);
|
|
414
|
+
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
415
|
+
return exitWith(output.partial === true ? 1 : 0);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return fail(error, hostNotice);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function fail(error, hostNotice) {
|
|
421
|
+
if (error instanceof UsageError) {
|
|
422
|
+
process.stderr.write(error.message + "\n");
|
|
423
|
+
return exitWith(2);
|
|
424
|
+
}
|
|
425
|
+
if (error instanceof LocalError) {
|
|
426
|
+
process.stderr.write(`harness: ${clean(error.message)}
|
|
427
|
+
`);
|
|
428
|
+
return exitWith(1);
|
|
429
|
+
}
|
|
430
|
+
if (error instanceof TransportError) {
|
|
431
|
+
process.stderr.write(hostNotice + `harness: cannot reach the API: ${clean(error.message)}
|
|
432
|
+
`);
|
|
433
|
+
return exitWith(3);
|
|
434
|
+
}
|
|
435
|
+
if (error instanceof UpgradeRequiredError) {
|
|
436
|
+
process.stderr.write(hostNotice + `harness: ${clean(error.message)} (HTTP 426)
|
|
437
|
+
`);
|
|
438
|
+
return exitWith(1);
|
|
439
|
+
}
|
|
440
|
+
if (error instanceof HttpError) {
|
|
441
|
+
process.stderr.write(
|
|
442
|
+
hostNotice + `harness: ${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}
|
|
443
|
+
`
|
|
444
|
+
);
|
|
445
|
+
return exitWith(1);
|
|
446
|
+
}
|
|
447
|
+
if (error instanceof CommandError) {
|
|
448
|
+
process.stderr.write(hostNotice + `harness: ${clean(error.message)}
|
|
449
|
+
`);
|
|
450
|
+
return exitWith(1);
|
|
451
|
+
}
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
function errorDetails(body) {
|
|
455
|
+
const details = body["details"];
|
|
456
|
+
if (typeof details !== "object" || details === null || Array.isArray(details)) return "";
|
|
457
|
+
return Object.entries(details).map(([field, messages]) => {
|
|
458
|
+
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
459
|
+
return `
|
|
460
|
+
${clean(field)}: ${clean(text)}`;
|
|
461
|
+
}).join("");
|
|
462
|
+
}
|
|
463
|
+
program.command("ready").description("issues ready to work, highest priority first").option("--claim", "atomically claim the first claimable issue").option("--team <key>", "team filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
464
|
+
await withConfig(opts.json === true, opts.claim === true, (ctx) => ready(ctx, opts));
|
|
465
|
+
});
|
|
466
|
+
program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
467
|
+
await withConfig(opts.json === true, false, (ctx) => blocked(ctx, opts));
|
|
468
|
+
});
|
|
469
|
+
program.command("list").description("list issues with filters").option("--status <status>", "open | in_progress | closed").option("--assignee <who>", 'me, a user id, an email, or a full name; "" for unassigned').option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
470
|
+
await withConfig(opts.json === true, false, (ctx) => list(ctx, opts));
|
|
471
|
+
});
|
|
472
|
+
program.command("search").description("full-text search").argument("[query...]").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (query, opts) => {
|
|
473
|
+
await withConfig(opts.json === true, false, (ctx) => search(ctx, query, opts));
|
|
474
|
+
});
|
|
475
|
+
program.command("show").description("full issue detail; downloads attached images locally").argument("[id]").option("--no-images", "skip attachment downloads").option("--json", "emit JSON").action(async (id, opts) => {
|
|
476
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
477
|
+
if (id === void 0) throw new UsageError("usage: harness show <id>");
|
|
478
|
+
requireIdentifiers2([id]);
|
|
479
|
+
return show(ctx, id, opts);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
program.command("stats").description("per-team and total counts").option("--json", "emit JSON").action(async (opts) => {
|
|
483
|
+
await withConfig(opts.json === true, false, (ctx) => stats(ctx));
|
|
484
|
+
});
|
|
485
|
+
program.command("prime").description("workflow context for agent sessions: stats + ready queue").option("--json", "emit JSON").action(async (opts) => {
|
|
486
|
+
await withConfig(opts.json === true, false, (ctx) => prime(ctx));
|
|
487
|
+
});
|
|
488
|
+
program.command("version").description("print CLI and server API versions").option("--json", "emit JSON").action(async (opts) => {
|
|
489
|
+
const resolved = resolveConfig();
|
|
490
|
+
let config = null;
|
|
491
|
+
if (resolved.kind === "ok") config = resolved.config;
|
|
492
|
+
else if (resolved.kind === "needs-login") {
|
|
493
|
+
config = { baseUrl: resolved.baseUrl, token: "", actor: null, team: null, cacheDir: "" };
|
|
494
|
+
}
|
|
495
|
+
const output = await version(config);
|
|
496
|
+
process.stdout.write(opts.json === true ? json(output.json) : output.human);
|
|
497
|
+
await exitWith(0);
|
|
498
|
+
});
|
|
499
|
+
program.command("create").description("create an issue").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--type <type>", "task | bug | feature | epic (applied as a type/ label)").option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option("--label <name>", "label to apply (repeatable)", collect, []).option("--json", "emit JSON").action(async (opts) => {
|
|
500
|
+
await withConfig(opts.json === true, true, (ctx) => create(ctx, opts));
|
|
501
|
+
});
|
|
502
|
+
program.command("update").description("update an issue; --claim takes it atomically").argument("[id]").option("--claim", "claim atomically and move to the first started state").option("--state <name>", "workflow state by name or type").option("--title <text>").option("--description <text>").option("--design <text>").option("--notes <text>", "replace the notes").option("--append-notes <text>", "append to the notes").option("--acceptance <text>").option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", 'YYYY-MM-DD; "" clears').option("--parent <id>").option("--assignee <who>", 'me, a user id, an email, or a full name; "" unassigns').option("--json", "emit JSON").action(async (id, opts) => {
|
|
503
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
504
|
+
if (id === void 0) {
|
|
505
|
+
throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
|
|
506
|
+
}
|
|
507
|
+
requireIdentifiers2([id]);
|
|
508
|
+
return update(ctx, id, opts);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
program.command("close").description("close issues; the response lists newly unblocked issues").argument("[ids...]").option("--reason <text>", "why it is done (e.g. the commit hash)").option("--cancel", "close as canceled instead of completed").option("--json", "emit JSON").action(async (ids, opts) => {
|
|
512
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
513
|
+
if (ids.length === 0) {
|
|
514
|
+
throw new UsageError("usage: harness close <id> [<id>\u2026] [--reason <text>] [--cancel]");
|
|
515
|
+
}
|
|
516
|
+
requireIdentifiers2(ids);
|
|
517
|
+
return close(ctx, ids, opts);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
program.command("archive").description("archive issues; --restore brings them back").argument("[ids...]").option("--restore", "restore instead of archive").option("--json", "emit JSON").action(async (ids, opts) => {
|
|
521
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
522
|
+
if (ids.length === 0) throw new UsageError("usage: harness archive <id> [<id>\u2026] [--restore]");
|
|
523
|
+
requireIdentifiers2(ids);
|
|
524
|
+
return archive(ctx, ids, opts);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
program.command("comments").description("an issue's full comment thread, untruncated").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
528
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
529
|
+
if (id === void 0) throw new UsageError("usage: harness comments <id>");
|
|
530
|
+
requireIdentifiers2([id]);
|
|
531
|
+
return comments(ctx, id);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
program.command("dep").description("blocking dependencies: dep add|rm <issue> <blocker>").argument("[verb]").argument("[issue]").argument("[blocker]").option("--json", "emit JSON").action(async (verb, issue, blocker, opts) => {
|
|
535
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
536
|
+
if (verb === void 0 || issue === void 0 || blocker === void 0) {
|
|
537
|
+
throw new UsageError("usage: harness dep add|rm <issue> <blocker>");
|
|
538
|
+
}
|
|
539
|
+
requireIdentifiers2([issue, blocker]);
|
|
540
|
+
return dep(ctx, verb, issue, blocker);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(async (id, text, opts) => {
|
|
544
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
545
|
+
if (id === void 0 || text.length === 0) {
|
|
546
|
+
throw new UsageError("usage: harness comment <id> <text> [--mention NAME]\u2026");
|
|
547
|
+
}
|
|
548
|
+
requireIdentifiers2([id]);
|
|
549
|
+
const mentions = opts.mention.filter((name) => name.trim() !== "");
|
|
550
|
+
return comment(ctx, id, text, mentions);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
program.command("attach-image").description("upload an image; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
|
|
554
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
555
|
+
if (path === void 0) {
|
|
556
|
+
throw new UsageError("usage: harness attach-image <path> (one png, jpg, webp or gif, up to 25 MB)");
|
|
557
|
+
}
|
|
558
|
+
return attachImage(ctx, path);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
var project = program.command("project").description("projects");
|
|
562
|
+
project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
563
|
+
await withConfig(opts.json === true, false, (ctx) => projectList(ctx, opts));
|
|
564
|
+
});
|
|
565
|
+
project.command("show").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
566
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
567
|
+
if (id === void 0) throw new UsageError("usage: harness project show <id>");
|
|
568
|
+
return projectShow(ctx, id);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
project.command("create").option("--name <name>", "project name (required)").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", "YYYY-MM-DD").option("--target-date <date>", "YYYY-MM-DD").option("--json", "emit JSON").action(async (opts) => {
|
|
572
|
+
await withConfig(opts.json === true, true, (ctx) => projectCreate(ctx, opts));
|
|
573
|
+
});
|
|
574
|
+
project.command("update").argument("[id]").option("--name <name>").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", 'YYYY-MM-DD; "" clears').option("--target-date <date>", 'YYYY-MM-DD; "" clears').option("--json", "emit JSON").action(async (id, opts) => {
|
|
575
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
576
|
+
if (id === void 0) throw new UsageError("usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026");
|
|
577
|
+
return projectUpdate(ctx, id, opts);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
var milestone = program.command("milestone").description("project milestones");
|
|
581
|
+
milestone.command("list").option("--project <id>", "project id (required)").option("--json", "emit JSON").action(async (opts) => {
|
|
582
|
+
await withConfig(opts.json === true, false, (ctx) => milestoneList(ctx, opts.project));
|
|
583
|
+
});
|
|
584
|
+
milestone.command("create").option("--project <id>", "project id (required)").option("--name <name>", "milestone name (required)").option("--description <text>").option("--target-date <date>", "YYYY-MM-DD").option("--json", "emit JSON").action(async (opts) => {
|
|
585
|
+
await withConfig(opts.json === true, true, (ctx) => milestoneCreate(ctx, opts));
|
|
586
|
+
});
|
|
587
|
+
var doc = program.command("doc").description("documents");
|
|
588
|
+
doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
589
|
+
await withConfig(opts.json === true, false, (ctx) => docList(ctx, opts));
|
|
590
|
+
});
|
|
591
|
+
doc.command("show").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
592
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
593
|
+
if (id === void 0) throw new UsageError("usage: harness doc show <id>");
|
|
594
|
+
return docShow(ctx, id);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
doc.command("create").option("--title <title>", "document title (required)").option("--content <text>").option("--project <id>", "attach to a project").option("--json", "emit JSON").action(async (opts) => {
|
|
598
|
+
await withConfig(opts.json === true, true, (ctx) => docCreate(ctx, opts));
|
|
599
|
+
});
|
|
600
|
+
doc.command("update").argument("[id]").option("--title <title>").option("--content <text>").option("--project <id>").option("--json", "emit JSON").action(async (id, opts) => {
|
|
601
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
602
|
+
if (id === void 0) throw new UsageError("usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026");
|
|
603
|
+
return docUpdate(ctx, id, opts);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
var link = program.command("link").description("URLs attached to an issue (distinct from dep relations)");
|
|
607
|
+
link.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
|
|
608
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
609
|
+
if (issue === void 0) throw new UsageError("usage: harness link list <issue>");
|
|
610
|
+
requireIdentifiers2([issue]);
|
|
611
|
+
return linkList(ctx, issue);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
link.command("add").argument("[issue]").argument("[url]").option("--title <title>", "link title").option("--json", "emit JSON").action(async (issue, url, opts) => {
|
|
615
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
616
|
+
if (issue === void 0 || url === void 0) {
|
|
617
|
+
throw new UsageError("usage: harness link add <issue> <url> [--title <title>]");
|
|
618
|
+
}
|
|
619
|
+
requireIdentifiers2([issue]);
|
|
620
|
+
return linkAdd(ctx, issue, url, opts.title);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(async (issue, idOrUrl, opts) => {
|
|
624
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
625
|
+
if (issue === void 0 || idOrUrl === void 0) {
|
|
626
|
+
throw new UsageError("usage: harness link rm <issue> <link-id-or-url>");
|
|
627
|
+
}
|
|
628
|
+
requireIdentifiers2([issue]);
|
|
629
|
+
return linkRm(ctx, issue, idOrUrl);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
var label = program.command("label").description("labels");
|
|
633
|
+
label.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
634
|
+
await withConfig(opts.json === true, false, (ctx) => labelList(ctx));
|
|
635
|
+
});
|
|
636
|
+
label.command("create").argument("[name]").option("--color <color>", "label color").option("--json", "emit JSON").action(async (name, opts) => {
|
|
637
|
+
await withConfig(opts.json === true, true, (ctx) => labelCreate(ctx, name, opts.color));
|
|
638
|
+
});
|
|
639
|
+
var team = program.command("team").description("teams");
|
|
640
|
+
team.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
641
|
+
await withConfig(opts.json === true, false, (ctx) => teamList(ctx));
|
|
642
|
+
});
|
|
643
|
+
program.command("states").description("a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
|
|
644
|
+
await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
|
|
645
|
+
});
|
|
646
|
+
var user = program.command("user").description("users");
|
|
647
|
+
user.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
648
|
+
await withConfig(opts.json === true, false, (ctx) => userList(ctx));
|
|
649
|
+
});
|
|
650
|
+
program.command("upload").description("upload a file; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
|
|
651
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
652
|
+
if (path === void 0) throw new UsageError("usage: harness upload <path> (png/jpg/webp/gif, up to 25 MB)");
|
|
653
|
+
return upload(ctx, path);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
657
|
+
const { startMcpServer } = await import("./server-PI2AZOVY.js");
|
|
658
|
+
await startMcpServer();
|
|
659
|
+
});
|
|
660
|
+
program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option("--timeout <ms>", "how long to wait for the browser callback", integer).option("--json", "emit JSON").action(async (opts) => {
|
|
661
|
+
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
|
|
662
|
+
const options = { noBrowser: !opts.browser };
|
|
663
|
+
if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
|
|
664
|
+
await finishAuth(await login(url, options), opts.json === true);
|
|
665
|
+
});
|
|
666
|
+
program.command("logout").description("revoke and forget the stored login").option("--json", "emit JSON").action(async (opts) => {
|
|
667
|
+
await finishAuth(await logout(), opts.json === true);
|
|
668
|
+
});
|
|
669
|
+
program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
|
|
670
|
+
const resolved = resolveConfig();
|
|
671
|
+
if (resolved.kind === "error") {
|
|
672
|
+
process.stderr.write(`harness: ${resolved.message}
|
|
673
|
+
`);
|
|
674
|
+
return exitWith(2);
|
|
675
|
+
}
|
|
676
|
+
if (resolved.kind === "needs-login") {
|
|
677
|
+
process.stderr.write(loginHelp(resolved.baseUrl));
|
|
678
|
+
return exitWith(2);
|
|
679
|
+
}
|
|
680
|
+
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
681
|
+
await finishAuth(await whoami(resolved.config, source), opts.json === true);
|
|
682
|
+
});
|
|
683
|
+
function storedLoginUrl() {
|
|
684
|
+
return readCredentials(defaultCredentialsPath())?.url;
|
|
685
|
+
}
|
|
686
|
+
function blankToUndefined(value) {
|
|
687
|
+
return value === void 0 || value.trim() === "" ? void 0 : value;
|
|
688
|
+
}
|
|
689
|
+
async function finishAuth(result, json2) {
|
|
690
|
+
if (result.ok) {
|
|
691
|
+
process.stdout.write(json2 ? json(result.json) : result.human);
|
|
692
|
+
return exitWith(0);
|
|
693
|
+
}
|
|
694
|
+
process.stderr.write(`harness: ${clean(result.message)}
|
|
695
|
+
`);
|
|
696
|
+
return exitWith(1);
|
|
697
|
+
}
|
|
698
|
+
var argv = process.argv.slice();
|
|
699
|
+
if (argv[2] === "--version" || argv[2] === "-V") argv[2] = "version";
|
|
700
|
+
if (argv.length <= 2) {
|
|
701
|
+
program.outputHelp({ error: true });
|
|
702
|
+
await exitWith(2);
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
await program.parseAsync(argv);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (error instanceof UsageError) {
|
|
708
|
+
process.stderr.write(error.message + "\n");
|
|
709
|
+
await exitWith(2);
|
|
710
|
+
}
|
|
711
|
+
if (error instanceof CommanderError) {
|
|
712
|
+
if (error.code === "commander.helpDisplayed" || error.code === "commander.help") {
|
|
713
|
+
await exitWith(error.exitCode === 0 ? 0 : 2);
|
|
714
|
+
}
|
|
715
|
+
await exitWith(2);
|
|
716
|
+
}
|
|
717
|
+
throw error;
|
|
718
|
+
}
|