@allocator-one/harness-cli 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -47
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-I2S2JVCQ.js → chunk-GVTZKAXC.js} +432 -107
- package/dist/cli.js +455 -158
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-O7CX2ZCC.js → server-F6YRKNC2.js} +78 -17
- package/package.json +1 -1
- package/spec/openapi.json +6593 -4421
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ACTIVITY_MAX_LIMIT,
|
|
4
|
-
CLI_PACKAGE,
|
|
5
|
-
CLI_VERSION,
|
|
6
4
|
Client,
|
|
7
5
|
CommandError,
|
|
8
6
|
DEFAULT_BASE_URL,
|
|
@@ -16,8 +14,11 @@ import {
|
|
|
16
14
|
JOBS_MAX_PAGES,
|
|
17
15
|
JOB_STATES,
|
|
18
16
|
LocalError,
|
|
17
|
+
PLAN_LIST_MAX_LIMIT,
|
|
18
|
+
PLAN_STATUSES,
|
|
19
19
|
RELATION_CREATE_TYPES,
|
|
20
20
|
RELATION_UPDATE_TYPES,
|
|
21
|
+
SESSION_PATH,
|
|
21
22
|
STEER_MAX_CHARACTERS,
|
|
22
23
|
TransportError,
|
|
23
24
|
UpgradeRequiredError,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
instructionFrom,
|
|
51
52
|
isExpertToken,
|
|
52
53
|
isHostTrusted,
|
|
54
|
+
isLoginCredential,
|
|
53
55
|
jobCancel,
|
|
54
56
|
jobContinue,
|
|
55
57
|
jobPause,
|
|
@@ -73,12 +75,15 @@ import {
|
|
|
73
75
|
memberSetRole,
|
|
74
76
|
milestoneCreate,
|
|
75
77
|
milestoneList,
|
|
78
|
+
orgCredentialsPath,
|
|
76
79
|
planCreate,
|
|
77
80
|
planEdit,
|
|
78
81
|
planHold,
|
|
82
|
+
planList,
|
|
79
83
|
planRelease,
|
|
80
84
|
planShow,
|
|
81
85
|
prepareProcedureRun,
|
|
86
|
+
prepareRun,
|
|
82
87
|
prime,
|
|
83
88
|
procedureSave,
|
|
84
89
|
procedureShow,
|
|
@@ -99,7 +104,8 @@ import {
|
|
|
99
104
|
requireIdentifiers,
|
|
100
105
|
requireJobId,
|
|
101
106
|
requireJobsArgs,
|
|
102
|
-
|
|
107
|
+
requirePlanListArgs,
|
|
108
|
+
requirePlanRef,
|
|
103
109
|
requireProcedureId,
|
|
104
110
|
requireProcedureName,
|
|
105
111
|
requireReadyClaimArgs,
|
|
@@ -110,6 +116,7 @@ import {
|
|
|
110
116
|
resolveCacheHome,
|
|
111
117
|
resolveConfig,
|
|
112
118
|
resolveTextSources,
|
|
119
|
+
run,
|
|
113
120
|
search,
|
|
114
121
|
show,
|
|
115
122
|
skillsList,
|
|
@@ -131,13 +138,21 @@ import {
|
|
|
131
138
|
update,
|
|
132
139
|
upload,
|
|
133
140
|
userList,
|
|
141
|
+
validOrgId,
|
|
134
142
|
validateBaseUrl,
|
|
135
143
|
validateIdempotencyKey,
|
|
136
144
|
version,
|
|
137
145
|
watchStopSignal,
|
|
138
146
|
workflowStates,
|
|
139
147
|
writeCredentials
|
|
140
|
-
} from "./chunk-
|
|
148
|
+
} from "./chunk-GVTZKAXC.js";
|
|
149
|
+
import {
|
|
150
|
+
CLI_VERSION,
|
|
151
|
+
OPERATOR_PACKAGE,
|
|
152
|
+
invokedName,
|
|
153
|
+
layoutOperatorHelp,
|
|
154
|
+
named
|
|
155
|
+
} from "./chunk-FQWV5MIC.js";
|
|
141
156
|
|
|
142
157
|
// src/cli.ts
|
|
143
158
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -267,8 +282,10 @@ async function exec(ctx, opts) {
|
|
|
267
282
|
const missing = unstoredSlugs(requested, stored);
|
|
268
283
|
if (missing.length > 0) {
|
|
269
284
|
process.stderr.write(
|
|
270
|
-
|
|
285
|
+
named(
|
|
286
|
+
`harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
|
|
271
287
|
`
|
|
288
|
+
)
|
|
272
289
|
);
|
|
273
290
|
}
|
|
274
291
|
}
|
|
@@ -325,6 +342,172 @@ async function exec(ctx, opts) {
|
|
|
325
342
|
return { json: { exit_code: exitCode }, human: "", exitCode };
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
// src/commands/review.ts
|
|
346
|
+
import { execFileSync } from "child_process";
|
|
347
|
+
function isRecord2(value) {
|
|
348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
349
|
+
}
|
|
350
|
+
var SLUG = /^([\w.-]+)\/([\w.-]+)$/;
|
|
351
|
+
function githubSlug(remote) {
|
|
352
|
+
const match = /^(?:(?:https?|ssh|git):\/\/(?:[^@/\s]+@)?|[\w.-]+@)github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i.exec(remote.trim());
|
|
353
|
+
return match ? `${match[1]}/${match[2]}` : void 0;
|
|
354
|
+
}
|
|
355
|
+
function originRemote() {
|
|
356
|
+
try {
|
|
357
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
358
|
+
return githubSlug(url);
|
|
359
|
+
} catch {
|
|
360
|
+
return void 0;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function prNumber(digits, input) {
|
|
364
|
+
const number = Number(digits);
|
|
365
|
+
if (!Number.isSafeInteger(number) || number < 1) throw new UsageError(`not a pull request number: ${input}`);
|
|
366
|
+
return number;
|
|
367
|
+
}
|
|
368
|
+
var NAME = /^[\w.-]+$/;
|
|
369
|
+
function fromUrl(input) {
|
|
370
|
+
let url;
|
|
371
|
+
try {
|
|
372
|
+
url = new URL(input);
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (!["https:", "http:"].includes(url.protocol) || !/^(www\.)?github\.com$/i.test(url.hostname)) return null;
|
|
377
|
+
let segments;
|
|
378
|
+
try {
|
|
379
|
+
segments = url.pathname.split("/").filter((segment) => segment !== "").map(decodeURIComponent);
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
const [owner, repo, kind, number] = segments;
|
|
384
|
+
if (kind !== "pull" || !owner || !repo || !number || !NAME.test(owner) || !NAME.test(repo) || !/^\d+$/.test(number)) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
return [owner, repo, number];
|
|
388
|
+
}
|
|
389
|
+
function parsePullRequest(input, repo, origin = originRemote) {
|
|
390
|
+
const trimmed = input.trim();
|
|
391
|
+
const url = fromUrl(trimmed);
|
|
392
|
+
if (url) return { owner: url[0], repo: url[1], number: prNumber(url[2], input) };
|
|
393
|
+
const named2 = /^([\w.-]+)\/([\w.-]+)#(\d+)$/.exec(trimmed);
|
|
394
|
+
if (named2) return { owner: named2[1], repo: named2[2], number: prNumber(named2[3], input) };
|
|
395
|
+
const bare = /^#?(\d+)$/.exec(trimmed);
|
|
396
|
+
if (!bare) throw new UsageError(`not a pull request: ${input} \u2014 use owner/repo#<pr>, its URL, or <pr> with --repo`);
|
|
397
|
+
const slug = repo ?? origin();
|
|
398
|
+
const parts = slug === void 0 ? null : SLUG.exec(slug.trim());
|
|
399
|
+
if (!parts) throw new UsageError("name the repository: owner/repo#<pr>, the pull request URL, or --repo owner/repo");
|
|
400
|
+
return { owner: parts[1], repo: parts[2], number: prNumber(bare[1], input) };
|
|
401
|
+
}
|
|
402
|
+
var label = (pr) => `${pr.owner}/${pr.repo}#${pr.number}`;
|
|
403
|
+
var gatePath = (pr) => `/api/v1/reviews/prs/${encodeURIComponent(pr.owner)}/${encodeURIComponent(pr.repo)}/${pr.number}`;
|
|
404
|
+
async function fetchGate(ctx, pr) {
|
|
405
|
+
const body = await ctx.client.get(gatePath(pr));
|
|
406
|
+
if (!isRecord2(body.data)) throw new CommandError(`unexpected gate status for ${label(pr)}: no data`);
|
|
407
|
+
return body.data;
|
|
408
|
+
}
|
|
409
|
+
function latestAttempt(lifecycle) {
|
|
410
|
+
const attempts = Array.isArray(lifecycle["attempts"]) ? lifecycle["attempts"].filter(isRecord2) : [];
|
|
411
|
+
return attempts.reduce(
|
|
412
|
+
(latest, attempt) => latest === void 0 || Number(attempt["number"]) > Number(latest["number"]) ? attempt : latest,
|
|
413
|
+
void 0
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function recovery(lifecycle) {
|
|
417
|
+
const latest = latestAttempt(lifecycle);
|
|
418
|
+
const sent = isRecord2(lifecycle["recovery"]) ? lifecycle["recovery"] : void 0;
|
|
419
|
+
const state2 = String(sent?.["state"] ?? latest?.["state"] ?? "");
|
|
420
|
+
if (state2 !== "failed" && state2 !== "timed_out") return void 0;
|
|
421
|
+
const reason = sent?.["terminal_reason"] ?? latest?.["terminal_reason"];
|
|
422
|
+
return {
|
|
423
|
+
number: latest?.["number"],
|
|
424
|
+
state: state2 === "timed_out" ? "timed out" : state2,
|
|
425
|
+
reason: typeof reason === "string" ? reason : null,
|
|
426
|
+
evidence: sent ? sent["evidence_recorded"] === true : latest?.["review_run_id"] != null,
|
|
427
|
+
next: sent?.["next"] === "automatic_retry" ? "automatic_retry" : "retry"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function describe(r) {
|
|
431
|
+
const why = [r.reason, r.evidence ? null : "no evidence recorded"].filter((part) => part !== null).join(", ");
|
|
432
|
+
return `attempt ${clean(String(r.number))} ${r.state}${why === "" ? "" : ` (${clean(why)})`}`;
|
|
433
|
+
}
|
|
434
|
+
function projection(gate2, key) {
|
|
435
|
+
const value = gate2[key];
|
|
436
|
+
return isRecord2(value) && typeof value["status"] === "string" ? clean(value["status"]) : "unknown";
|
|
437
|
+
}
|
|
438
|
+
async function status(ctx, pr) {
|
|
439
|
+
const gate2 = await fetchGate(ctx, pr);
|
|
440
|
+
const lifecycle = isRecord2(gate2["lifecycle"]) ? gate2["lifecycle"] : void 0;
|
|
441
|
+
const head = isRecord2(gate2["head"]) && typeof gate2["head"]["sha"] === "string" ? gate2["head"]["sha"].slice(0, 10) : "?";
|
|
442
|
+
const lines = [
|
|
443
|
+
`${label(pr)} at ${clean(head)}`,
|
|
444
|
+
`enforced: ${projection(gate2, "enforced")} advisory: ${projection(gate2, "advisory")}`
|
|
445
|
+
];
|
|
446
|
+
if (lifecycle) {
|
|
447
|
+
lines.push(`gate check: ${clean(String(lifecycle["state"] ?? "unknown")).replaceAll("_", " ")}`);
|
|
448
|
+
const r = recovery(lifecycle);
|
|
449
|
+
if (r) {
|
|
450
|
+
lines.push(`latest ${describe(r)}`);
|
|
451
|
+
lines.push(
|
|
452
|
+
r.next === "automatic_retry" ? "Harness retries it automatically; check again shortly." : named(`Retry it with: harness review retry ${label(pr)} --reason "\u2026"`)
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { json: gate2, human: lines.join("\n") + "\n" };
|
|
457
|
+
}
|
|
458
|
+
var REFUSALS = {
|
|
459
|
+
execution_not_terminal: "the latest attempt is still running; wait for it to finish",
|
|
460
|
+
attempt_changed: "another attempt was created meanwhile; read the status and run this again",
|
|
461
|
+
check_changed: "the head's gate check changed meanwhile; run this again",
|
|
462
|
+
head_changed: "the pull request's head moved; the new head gets its own check",
|
|
463
|
+
advisory_not_converged: "the head's advisory review has not converged; converge it first",
|
|
464
|
+
retry_conflict: "this attempt was already retried with a different reason",
|
|
465
|
+
merged: "the pull request is merged"
|
|
466
|
+
};
|
|
467
|
+
async function retry(ctx, pr, reason) {
|
|
468
|
+
const gate2 = await fetchGate(ctx, pr);
|
|
469
|
+
const lifecycle = isRecord2(gate2["lifecycle"]) ? gate2["lifecycle"] : void 0;
|
|
470
|
+
if (!lifecycle || typeof lifecycle["check_id"] !== "string" || typeof lifecycle["head_sha"] !== "string") {
|
|
471
|
+
throw new CommandError(`${label(pr)} has no gate check at its current head: nothing to retry`);
|
|
472
|
+
}
|
|
473
|
+
const r = recovery(lifecycle);
|
|
474
|
+
if (r?.next === "automatic_retry") {
|
|
475
|
+
throw new CommandError(named(`Harness retries attempt ${clean(String(r.number))} itself; run harness review status ${label(pr)} shortly`));
|
|
476
|
+
}
|
|
477
|
+
const previous = latestAttempt(lifecycle);
|
|
478
|
+
const previousId = typeof previous?.["id"] === "string" ? previous["id"] : null;
|
|
479
|
+
const body = {
|
|
480
|
+
head_sha: lifecycle["head_sha"],
|
|
481
|
+
check_id: lifecycle["check_id"],
|
|
482
|
+
previous_attempt_id: previousId,
|
|
483
|
+
// One key per attempt retried: running this again for the same attempt returns the attempt already
|
|
484
|
+
// queued instead of paying for another review run.
|
|
485
|
+
request_key: `harness-cli:${previousId ?? lifecycle["check_id"]}`,
|
|
486
|
+
reason: reason?.trim() || "Retry requested with harness review retry"
|
|
487
|
+
};
|
|
488
|
+
let response;
|
|
489
|
+
try {
|
|
490
|
+
response = await ctx.client.post(`${gatePath(pr)}/retry`, body, { actor: false });
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error instanceof HttpError && error.status === 409) {
|
|
493
|
+
const code = error.message.trim().split(/[\s:]+/).pop() ?? "";
|
|
494
|
+
throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[code] ?? clean(error.message)}`);
|
|
495
|
+
}
|
|
496
|
+
if (error instanceof HttpError && error.status === 403) {
|
|
497
|
+
throw new CommandError(`${error.message} \u2014 a retry needs a login with reviews:write`);
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
const attempt = isRecord2(response.data) ? response.data : {};
|
|
502
|
+
const outcome = response["outcome"];
|
|
503
|
+
const verb = outcome === "existing" ? "Already queued" : "Queued";
|
|
504
|
+
return {
|
|
505
|
+
json: { attempt, outcome },
|
|
506
|
+
human: `${verb} attempt ${clean(String(attempt["number"]))} (${clean(String(attempt["id"]))}) for ${label(pr)}
|
|
507
|
+
`
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
328
511
|
// src/core/auth.ts
|
|
329
512
|
import { createHash, randomBytes } from "crypto";
|
|
330
513
|
import { execFile } from "child_process";
|
|
@@ -338,9 +521,10 @@ var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
|
|
|
338
521
|
var MAX_PAGE_RESPONSES = 4;
|
|
339
522
|
var loginPageImport;
|
|
340
523
|
function loadLoginPage() {
|
|
341
|
-
loginPageImport ??= import("./login-page-
|
|
524
|
+
loginPageImport ??= import("./login-page-KGEJFFXR.js");
|
|
342
525
|
return loginPageImport;
|
|
343
526
|
}
|
|
527
|
+
var NO_SESSION = { kind: null, expires_at: null, scopes: null, user: null, org: null };
|
|
344
528
|
function generateState() {
|
|
345
529
|
return randomBytes(24).toString("base64url");
|
|
346
530
|
}
|
|
@@ -353,6 +537,9 @@ function challengeFor(verifier) {
|
|
|
353
537
|
async function login(baseUrl, options = {}) {
|
|
354
538
|
const validated = validateBaseUrl(baseUrl);
|
|
355
539
|
if (!validated.ok) return { ok: false, message: validated.message };
|
|
540
|
+
if (options.org !== void 0 && !validOrgId(options.org)) {
|
|
541
|
+
return { ok: false, message: "org must be an organization UUID" };
|
|
542
|
+
}
|
|
356
543
|
const base = baseUrl.replace(/\/+$/, "");
|
|
357
544
|
if (options.noBrowser === true || !browserAvailable(options)) {
|
|
358
545
|
return manualLogin(base, options);
|
|
@@ -425,6 +612,7 @@ async function loopbackLogin(baseUrl, options) {
|
|
|
425
612
|
const detail = error instanceof Error ? error.message : String(error);
|
|
426
613
|
return { ok: false, message: `could not open a browser: ${detail}` };
|
|
427
614
|
}
|
|
615
|
+
if (options.org !== void 0) notify(`Choose organization ${options.org} on the login page; another organization's token will be refused.`);
|
|
428
616
|
notify(`Opening ${url}`);
|
|
429
617
|
notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
|
|
430
618
|
const callback = await callbackPromise;
|
|
@@ -439,7 +627,7 @@ async function loopbackLogin(baseUrl, options) {
|
|
|
439
627
|
);
|
|
440
628
|
return token;
|
|
441
629
|
}
|
|
442
|
-
const result = await verifyAndStore(baseUrl, token.token, options);
|
|
630
|
+
const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
|
|
443
631
|
if (result.ok) {
|
|
444
632
|
await callback.respond(200, "You can close this browser and return to the terminal.", "success");
|
|
445
633
|
} else {
|
|
@@ -492,7 +680,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
492
680
|
if (phase !== "waiting") return;
|
|
493
681
|
const timeoutResponse = {
|
|
494
682
|
status: 410,
|
|
495
|
-
message:
|
|
683
|
+
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
496
684
|
pageState: "error"
|
|
497
685
|
};
|
|
498
686
|
phase = "terminal";
|
|
@@ -511,15 +699,15 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
511
699
|
});
|
|
512
700
|
server.on("request", (request, response) => {
|
|
513
701
|
const respond = callbackResponder(response);
|
|
514
|
-
const respondWithPage = (
|
|
702
|
+
const respondWithPage = (status2, message, pageState) => {
|
|
515
703
|
if (callbackPageResponses >= MAX_PAGE_RESPONSES) {
|
|
516
704
|
respondEmpty(response, 429);
|
|
517
705
|
return Promise.resolve();
|
|
518
706
|
}
|
|
519
707
|
callbackPageResponses += 1;
|
|
520
|
-
return respond(
|
|
708
|
+
return respond(status2, message, pageState);
|
|
521
709
|
};
|
|
522
|
-
const respondAtRoot = (
|
|
710
|
+
const respondAtRoot = (status2, message, pageState) => {
|
|
523
711
|
const rootPhase = phase === "terminal" ? "terminal" : "waiting";
|
|
524
712
|
const responses = rootPageResponses.get(rootPhase) ?? 0;
|
|
525
713
|
if (responses >= MAX_PAGE_RESPONSES) {
|
|
@@ -527,7 +715,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
527
715
|
return Promise.resolve();
|
|
528
716
|
}
|
|
529
717
|
rootPageResponses.set(rootPhase, responses + 1);
|
|
530
|
-
return respond(
|
|
718
|
+
return respond(status2, message, pageState);
|
|
531
719
|
};
|
|
532
720
|
let url;
|
|
533
721
|
try {
|
|
@@ -564,7 +752,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
564
752
|
}
|
|
565
753
|
const result = finalResponse ?? {
|
|
566
754
|
status: 410,
|
|
567
|
-
message:
|
|
755
|
+
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
568
756
|
pageState: "error"
|
|
569
757
|
};
|
|
570
758
|
void trackResponse(respondWithPage(result.status, result.message, result.pageState));
|
|
@@ -611,12 +799,12 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
611
799
|
resolve({
|
|
612
800
|
ok: true,
|
|
613
801
|
code,
|
|
614
|
-
respond: async (
|
|
615
|
-
finalResponse = { status, message, pageState };
|
|
802
|
+
respond: async (status2, message, pageState) => {
|
|
803
|
+
finalResponse = { status: status2, message, pageState };
|
|
616
804
|
phase = "terminal";
|
|
617
805
|
const responders = acceptedResponders.splice(0);
|
|
618
806
|
await Promise.all(
|
|
619
|
-
responders.map((acceptedRespond) => trackResponse(acceptedRespond(
|
|
807
|
+
responders.map((acceptedRespond) => trackResponse(acceptedRespond(status2, message, pageState)))
|
|
620
808
|
);
|
|
621
809
|
await drainTrackedResponses();
|
|
622
810
|
}
|
|
@@ -628,7 +816,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
628
816
|
});
|
|
629
817
|
}
|
|
630
818
|
function callbackResponder(response) {
|
|
631
|
-
return async (
|
|
819
|
+
return async (status2, message, state2) => {
|
|
632
820
|
if (response.writableEnded || response.destroyed) return;
|
|
633
821
|
return new Promise((resolve) => {
|
|
634
822
|
let flushTimer;
|
|
@@ -659,7 +847,7 @@ function callbackResponder(response) {
|
|
|
659
847
|
}
|
|
660
848
|
try {
|
|
661
849
|
const html = loginPage(message, state2);
|
|
662
|
-
response.writeHead(
|
|
850
|
+
response.writeHead(status2, {
|
|
663
851
|
"cache-control": "no-store",
|
|
664
852
|
connection: "close",
|
|
665
853
|
"content-length": String(Buffer.byteLength(html)),
|
|
@@ -679,10 +867,10 @@ function callbackResponder(response) {
|
|
|
679
867
|
});
|
|
680
868
|
};
|
|
681
869
|
}
|
|
682
|
-
function respondEmpty(response,
|
|
870
|
+
function respondEmpty(response, status2) {
|
|
683
871
|
response.once("error", () => void 0);
|
|
684
872
|
try {
|
|
685
|
-
response.writeHead(
|
|
873
|
+
response.writeHead(status2, {
|
|
686
874
|
"cache-control": "no-store",
|
|
687
875
|
connection: "close",
|
|
688
876
|
"x-content-type-options": "nosniff"
|
|
@@ -696,7 +884,8 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
696
884
|
try {
|
|
697
885
|
const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
|
|
698
886
|
const token = body.data?.token;
|
|
699
|
-
|
|
887
|
+
const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
|
|
888
|
+
if (typeof token === "string" && token !== "") return { ok: true, token, kind };
|
|
700
889
|
return {
|
|
701
890
|
ok: false,
|
|
702
891
|
message: "the login-code exchange returned no token \u2014 aborting"
|
|
@@ -720,25 +909,40 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
720
909
|
async function manualLogin(baseUrl, options) {
|
|
721
910
|
const notify = options.notify ?? defaultNotify;
|
|
722
911
|
const prompt = options.prompt ?? defaultPrompt;
|
|
912
|
+
if (options.org !== void 0) notify(`Choose organization ${options.org} on the login page; another organization's token will be refused.`);
|
|
723
913
|
notify(
|
|
724
914
|
`Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below (an outside expert pastes the pxc_ credential minted in their expert view instead).`
|
|
725
915
|
);
|
|
726
916
|
const token = (await prompt()).trim();
|
|
727
917
|
if (token === "") return { ok: false, message: "no token entered" };
|
|
728
|
-
return verifyAndStore(baseUrl, token, options);
|
|
918
|
+
return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
|
|
729
919
|
}
|
|
730
|
-
async function verifyAndStore(baseUrl, token, options) {
|
|
731
|
-
const
|
|
920
|
+
async function verifyAndStore(baseUrl, token, options, received) {
|
|
921
|
+
const defaultPath = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
922
|
+
const path = options.org === void 0 ? defaultPath : orgCredentialsPath(defaultPath, baseUrl, options.org);
|
|
732
923
|
const client = bareClient(baseUrl, token, options);
|
|
733
924
|
const expert = isExpertToken(token);
|
|
925
|
+
let kind = received.kind;
|
|
734
926
|
try {
|
|
735
|
-
|
|
927
|
+
if (expert) {
|
|
928
|
+
if (options.org !== void 0) return { ok: false, message: "cannot verify an outside expert credential against a repo org \u2014 nothing was stored" };
|
|
929
|
+
await client.get(EXPERT_ASSIGNMENTS_PATH);
|
|
930
|
+
} else {
|
|
931
|
+
const described = isLoginCredential(token) ? await describeSession(client) : null;
|
|
932
|
+
if (described !== null) kind = described.session.kind ?? kind;
|
|
933
|
+
if (options.org !== void 0 && (typeof described?.session.org?.["id"] !== "string" || described.session.org["id"].toLowerCase() !== options.org.toLowerCase())) {
|
|
934
|
+
const revoked = received.minted && await revokeSession(client, token);
|
|
935
|
+
return { ok: false, message: `the received token does not verify organization ${options.org} \u2014 nothing was stored` + (revoked ? ", and the token was revoked" : "") };
|
|
936
|
+
}
|
|
937
|
+
if (described === null) await client.get("/api/v1/teams");
|
|
938
|
+
}
|
|
736
939
|
} catch (error) {
|
|
737
940
|
if (error instanceof HttpError) {
|
|
738
941
|
const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
|
|
942
|
+
const revoked = received.minted && await revokeSession(client, token);
|
|
739
943
|
return {
|
|
740
944
|
ok: false,
|
|
741
|
-
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${lapsed}`
|
|
945
|
+
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${revoked ? ", and the token was revoked" : ""}${lapsed}`
|
|
742
946
|
};
|
|
743
947
|
}
|
|
744
948
|
if (error instanceof TransportError) {
|
|
@@ -754,11 +958,11 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
754
958
|
try {
|
|
755
959
|
writeCredentials(path, baseUrl, token);
|
|
756
960
|
} catch (error) {
|
|
757
|
-
await revokeSession(client, token);
|
|
961
|
+
if (received.minted) await revokeSession(client, token);
|
|
758
962
|
const detail = error instanceof Error ? error.message : String(error);
|
|
759
963
|
return {
|
|
760
964
|
ok: false,
|
|
761
|
-
message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : "")
|
|
965
|
+
message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : received.minted ? "" : " (the pasted token was not revoked and stays valid)")
|
|
762
966
|
};
|
|
763
967
|
}
|
|
764
968
|
let supersededNote = "";
|
|
@@ -768,12 +972,40 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
768
972
|
supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
|
|
769
973
|
}
|
|
770
974
|
}
|
|
975
|
+
const name = invokedName();
|
|
771
976
|
return {
|
|
772
977
|
ok: true,
|
|
773
|
-
json: { url: baseUrl, credentials: path },
|
|
774
|
-
human: (expert ? `Stored an outside expert's pxc_ credential for ${baseUrl} at ${path} (it lasts at most 30 days; revoke it in your expert view \u2014
|
|
775
|
-
` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days;
|
|
776
|
-
`) + supersededNote
|
|
978
|
+
json: { url: baseUrl, credentials: path, kind },
|
|
979
|
+
human: (expert ? `Stored an outside expert's pxc_ credential for ${baseUrl} at ${path} (it lasts at most 30 days; revoke it in your expert view \u2014 ${name} logout only forgets it). Commands now act as the expert: only assignments, conditions and gate work until you log in again.
|
|
980
|
+
` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; ${name} logout revokes it).
|
|
981
|
+
`) + kindNote(kind) + supersededNote
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function kindNote(kind) {
|
|
985
|
+
if (kind === "operator-cli") {
|
|
986
|
+
return "This credential is Operator-only (operator-cli): Operator commands work; issue, plan and gate commands are refused.\n";
|
|
987
|
+
}
|
|
988
|
+
if (kind === "harness-cli") return "This credential has full access (harness-cli).\n";
|
|
989
|
+
return "";
|
|
990
|
+
}
|
|
991
|
+
async function describeSession(client) {
|
|
992
|
+
try {
|
|
993
|
+
const { envelope, apiVersion } = await client.getVersioned(SESSION_PATH);
|
|
994
|
+
return { session: sessionOf(envelope.data), apiVersion };
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (error instanceof HttpError && (error.status === 404 || error.status === 405)) return null;
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function sessionOf(data) {
|
|
1001
|
+
if (!isRecord3(data)) return NO_SESSION;
|
|
1002
|
+
const text = (value) => typeof value === "string" ? value : null;
|
|
1003
|
+
return {
|
|
1004
|
+
kind: text(data["kind"]),
|
|
1005
|
+
expires_at: text(data["expires_at"]),
|
|
1006
|
+
scopes: Array.isArray(data["scopes"]) ? data["scopes"].filter((scope) => typeof scope === "string") : null,
|
|
1007
|
+
user: isRecord3(data["user"]) ? data["user"] : null,
|
|
1008
|
+
org: isRecord3(data["org"]) ? data["org"] : null
|
|
777
1009
|
};
|
|
778
1010
|
}
|
|
779
1011
|
async function revokeSession(client, token) {
|
|
@@ -814,8 +1046,18 @@ async function whoami(config, credentialSource) {
|
|
|
814
1046
|
const client = new Client(config);
|
|
815
1047
|
const expert = isExpertToken(config.token);
|
|
816
1048
|
let probe;
|
|
1049
|
+
let session = null;
|
|
1050
|
+
let serverVersion;
|
|
817
1051
|
try {
|
|
818
|
-
|
|
1052
|
+
const described2 = isLoginCredential(config.token) ? await describeSession(client) : null;
|
|
1053
|
+
if (described2 !== null) {
|
|
1054
|
+
session = described2.session;
|
|
1055
|
+
serverVersion = described2.apiVersion;
|
|
1056
|
+
} else {
|
|
1057
|
+
const answered = await client.getVersioned(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats");
|
|
1058
|
+
probe = answered.envelope.data;
|
|
1059
|
+
serverVersion = answered.apiVersion;
|
|
1060
|
+
}
|
|
819
1061
|
} catch (error) {
|
|
820
1062
|
if (error instanceof HttpError) {
|
|
821
1063
|
return {
|
|
@@ -835,7 +1077,7 @@ async function whoami(config, credentialSource) {
|
|
|
835
1077
|
};
|
|
836
1078
|
}
|
|
837
1079
|
}
|
|
838
|
-
|
|
1080
|
+
serverVersion ??= "unknown";
|
|
839
1081
|
const hostSourceLabels = {
|
|
840
1082
|
env: "HARNESS_API_URL",
|
|
841
1083
|
"repo-config": ".harness-cli/config.json",
|
|
@@ -846,6 +1088,7 @@ async function whoami(config, credentialSource) {
|
|
|
846
1088
|
const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
|
|
847
1089
|
Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
|
|
848
1090
|
`;
|
|
1091
|
+
const described = session ?? NO_SESSION;
|
|
849
1092
|
return {
|
|
850
1093
|
ok: true,
|
|
851
1094
|
json: {
|
|
@@ -854,21 +1097,55 @@ Reaches: its own assignments and the gates it owns on them (assignments, con
|
|
|
854
1097
|
verified: true,
|
|
855
1098
|
server_version: serverVersion,
|
|
856
1099
|
host_source: config.hostSource,
|
|
1100
|
+
kind: described.kind,
|
|
1101
|
+
expires_at: described.expires_at,
|
|
1102
|
+
scopes: described.scopes,
|
|
1103
|
+
user: described.user,
|
|
1104
|
+
org: described.org,
|
|
857
1105
|
expert: seat
|
|
858
1106
|
},
|
|
859
1107
|
human: `Host: ${config.baseUrl}${via}
|
|
860
|
-
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}
|
|
861
|
-
` + expertLines + `Verified: ok (server API ${serverVersion})
|
|
1108
|
+
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}${kindLabel(described.kind)}
|
|
1109
|
+
` + sessionLines(described) + expertLines + `Verified: ok (server API ${serverVersion})
|
|
862
1110
|
`
|
|
863
1111
|
};
|
|
864
1112
|
}
|
|
1113
|
+
function kindLabel(kind) {
|
|
1114
|
+
if (kind === null) return "";
|
|
1115
|
+
if (kind === "harness-cli") return ", harness-cli (full access)";
|
|
1116
|
+
if (kind === "operator-cli") return ", operator-cli (Operator only)";
|
|
1117
|
+
return `, ${clean(kind)}`;
|
|
1118
|
+
}
|
|
1119
|
+
function sessionLines(session) {
|
|
1120
|
+
let lines = "";
|
|
1121
|
+
const user2 = session.user;
|
|
1122
|
+
if (user2 !== null) {
|
|
1123
|
+
const name = clean(user2["name"]);
|
|
1124
|
+
const email = clean(user2["email"]);
|
|
1125
|
+
lines += `User: ${name === "" ? email : email === "" ? name : `${name} <${email}>`}
|
|
1126
|
+
`;
|
|
1127
|
+
}
|
|
1128
|
+
const org = session.org;
|
|
1129
|
+
if (org !== null) {
|
|
1130
|
+
const name = clean(org["name"]);
|
|
1131
|
+
const slug = clean(org["slug"]);
|
|
1132
|
+
lines += `Org: ${slug === "" || slug === name ? name : `${name} (${slug})`}
|
|
1133
|
+
`;
|
|
1134
|
+
}
|
|
1135
|
+
if (session.expires_at !== null) lines += `Expires: ${clean(session.expires_at)}
|
|
1136
|
+
`;
|
|
1137
|
+
if (session.kind === "operator-cli") {
|
|
1138
|
+
lines += "Reaches: Operator commands only; issue, plan and gate commands are refused\n";
|
|
1139
|
+
}
|
|
1140
|
+
return lines;
|
|
1141
|
+
}
|
|
865
1142
|
function expertSeat(data) {
|
|
866
|
-
if (!
|
|
867
|
-
const plan2 =
|
|
868
|
-
const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(
|
|
1143
|
+
if (!isRecord3(data) || !isRecord3(data["expert"])) return null;
|
|
1144
|
+
const plan2 = isRecord3(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
|
|
1145
|
+
const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord3).length : 0;
|
|
869
1146
|
return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
|
|
870
1147
|
}
|
|
871
|
-
function
|
|
1148
|
+
function isRecord3(value) {
|
|
872
1149
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
873
1150
|
}
|
|
874
1151
|
function bareClient(baseUrl, token, options) {
|
|
@@ -883,7 +1160,7 @@ function bareClient(baseUrl, token, options) {
|
|
|
883
1160
|
return new Client(config);
|
|
884
1161
|
}
|
|
885
1162
|
function defaultNotify(line) {
|
|
886
|
-
process.stderr.write(line + "\n");
|
|
1163
|
+
process.stderr.write(named(line + "\n"));
|
|
887
1164
|
}
|
|
888
1165
|
function defaultPrompt() {
|
|
889
1166
|
const readline = createInterface({
|
|
@@ -914,57 +1191,6 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
|
914
1191
|
}
|
|
915
1192
|
}
|
|
916
1193
|
|
|
917
|
-
// src/invocation.ts
|
|
918
|
-
import { basename } from "path";
|
|
919
|
-
var OPERATOR_PACKAGE = "@allocator-one/operator";
|
|
920
|
-
function invokedName(argv1 = process.argv[1], packageName = CLI_PACKAGE) {
|
|
921
|
-
const typed = basename(argv1 ?? "").replace(/\.[cm]?js$/i, "").toLowerCase();
|
|
922
|
-
if (typed === "operator" || typed === "harness") return typed;
|
|
923
|
-
return packageName === OPERATOR_PACKAGE ? "operator" : "harness";
|
|
924
|
-
}
|
|
925
|
-
var OPERATOR_COMMANDS = [
|
|
926
|
-
"run",
|
|
927
|
-
"jobs",
|
|
928
|
-
"job",
|
|
929
|
-
"watch",
|
|
930
|
-
"steer",
|
|
931
|
-
"cancel",
|
|
932
|
-
"pause",
|
|
933
|
-
"continue",
|
|
934
|
-
"procedure",
|
|
935
|
-
"plan",
|
|
936
|
-
"gate",
|
|
937
|
-
"conditions",
|
|
938
|
-
"assignments",
|
|
939
|
-
"tools",
|
|
940
|
-
"tool",
|
|
941
|
-
"skills",
|
|
942
|
-
"mcp"
|
|
943
|
-
];
|
|
944
|
-
var GENERAL_COMMANDS = [
|
|
945
|
-
"login",
|
|
946
|
-
"logout",
|
|
947
|
-
"whoami",
|
|
948
|
-
"version",
|
|
949
|
-
"keys"
|
|
950
|
-
];
|
|
951
|
-
var OPERATOR_HEADING = "Operator:";
|
|
952
|
-
var ISSUES_HEADING = "Issues:";
|
|
953
|
-
var GENERAL_HEADING = "General:";
|
|
954
|
-
function layoutOperatorHelp(program2) {
|
|
955
|
-
const rank = (name) => {
|
|
956
|
-
const operator = OPERATOR_COMMANDS.indexOf(name);
|
|
957
|
-
if (operator !== -1) return operator;
|
|
958
|
-
return GENERAL_COMMANDS.includes(name) ? 2 * OPERATOR_COMMANDS.length : OPERATOR_COMMANDS.length;
|
|
959
|
-
};
|
|
960
|
-
const heading = (name) => OPERATOR_COMMANDS.includes(name) ? OPERATOR_HEADING : GENERAL_COMMANDS.includes(name) ? GENERAL_HEADING : ISSUES_HEADING;
|
|
961
|
-
for (const command of program2.commands) command.helpGroup(heading(command.name()));
|
|
962
|
-
program2.commands.sort(
|
|
963
|
-
(left, right) => rank(left.name()) - rank(right.name())
|
|
964
|
-
);
|
|
965
|
-
program2.commandsGroup(GENERAL_HEADING).helpCommand(true);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
1194
|
// src/core/upgrade.ts
|
|
969
1195
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
970
1196
|
import {
|
|
@@ -980,7 +1206,7 @@ import {
|
|
|
980
1206
|
statSync,
|
|
981
1207
|
writeFileSync
|
|
982
1208
|
} from "fs";
|
|
983
|
-
import { basename
|
|
1209
|
+
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
984
1210
|
import { fileURLToPath } from "url";
|
|
985
1211
|
var PACKAGE_NAME = "@allocator-one/harness-cli";
|
|
986
1212
|
var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
|
|
@@ -1053,7 +1279,7 @@ function enclosingNodeModules(packageRoot) {
|
|
|
1053
1279
|
let dir = dirname(packageRoot);
|
|
1054
1280
|
const { root } = parse(dir);
|
|
1055
1281
|
for (; ; ) {
|
|
1056
|
-
if (
|
|
1282
|
+
if (basename(dir) === "node_modules") return dir;
|
|
1057
1283
|
if (dir === root) return null;
|
|
1058
1284
|
dir = dirname(dir);
|
|
1059
1285
|
}
|
|
@@ -1299,8 +1525,8 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1299
1525
|
};
|
|
1300
1526
|
}
|
|
1301
1527
|
process.stderr.write(
|
|
1302
|
-
`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1303
|
-
`
|
|
1528
|
+
named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1529
|
+
`)
|
|
1304
1530
|
);
|
|
1305
1531
|
const args = [
|
|
1306
1532
|
"install",
|
|
@@ -1368,8 +1594,8 @@ function reexecSelf(env, argv2) {
|
|
|
1368
1594
|
});
|
|
1369
1595
|
if (result.error !== void 0) {
|
|
1370
1596
|
process.stderr.write(
|
|
1371
|
-
`harness: upgraded, but re-running failed: ${result.error.message}
|
|
1372
|
-
`
|
|
1597
|
+
named(`harness: upgraded, but re-running failed: ${result.error.message}
|
|
1598
|
+
`)
|
|
1373
1599
|
);
|
|
1374
1600
|
return 1;
|
|
1375
1601
|
}
|
|
@@ -1473,11 +1699,14 @@ function exitWith(code) {
|
|
|
1473
1699
|
guardBrokenPipes();
|
|
1474
1700
|
var program = new Command();
|
|
1475
1701
|
var invokedAs = invokedName();
|
|
1702
|
+
function writeStderr(text) {
|
|
1703
|
+
return process.stderr.write(named(text, invokedAs));
|
|
1704
|
+
}
|
|
1476
1705
|
program.name(invokedAs).description(
|
|
1477
1706
|
invokedAs === "operator" ? "drive Operator from the terminal and any agent, with Harness issue tracking alongside (harness.infra.one)" : "issue tracking for humans and agents (harness.infra.one)"
|
|
1478
1707
|
).exitOverride().configureOutput({
|
|
1479
|
-
writeOut: (text) => process.stdout.write(text),
|
|
1480
|
-
writeErr: (text) =>
|
|
1708
|
+
writeOut: (text) => process.stdout.write(named(text, invokedAs)),
|
|
1709
|
+
writeErr: (text) => writeStderr(text)
|
|
1481
1710
|
});
|
|
1482
1711
|
function integer(value) {
|
|
1483
1712
|
if (!/^\d+$/.test(value))
|
|
@@ -1518,59 +1747,67 @@ function interactive() {
|
|
|
1518
1747
|
const ci = process.env["CI"];
|
|
1519
1748
|
return process.stdout.isTTY === true && (ci === void 0 || ci === "");
|
|
1520
1749
|
}
|
|
1521
|
-
|
|
1750
|
+
function canConfirmRepoLogin() {
|
|
1751
|
+
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
1752
|
+
}
|
|
1753
|
+
async function confirmRepoLogin(baseUrl, source, path) {
|
|
1754
|
+
if (source !== "repo-config" || isHostTrusted(baseUrl)) return true;
|
|
1755
|
+
const answer = await promptLine(
|
|
1756
|
+
`This repo's config (${path ?? ".harness-cli/config.json"}) targets ${baseUrl} \u2014 log in there? [y/N] `
|
|
1757
|
+
);
|
|
1758
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
1759
|
+
writeStderr(`harness: not logging in to ${baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
1760
|
+
`);
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
trustHost(baseUrl);
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
async function withConfig(jsonFlag, mutating, run2) {
|
|
1522
1767
|
const resolved = resolveConfig();
|
|
1523
|
-
for (const warning of resolved.warnings)
|
|
1768
|
+
for (const warning of resolved.warnings) writeStderr(warning + "\n");
|
|
1524
1769
|
if (resolved.kind === "error") {
|
|
1525
|
-
|
|
1770
|
+
writeStderr(`harness: ${resolved.message}
|
|
1526
1771
|
`);
|
|
1527
1772
|
return exitWith(2);
|
|
1528
1773
|
}
|
|
1529
1774
|
if (resolved.kind === "needs-login") {
|
|
1530
1775
|
if (!interactive()) {
|
|
1531
|
-
|
|
1776
|
+
writeStderr(loginHelp(resolved.baseUrl));
|
|
1777
|
+
if (resolved.org !== void 0) writeStderr(`Choose organization ${resolved.org} when logging in.
|
|
1778
|
+
`);
|
|
1532
1779
|
return exitWith(2);
|
|
1533
1780
|
}
|
|
1534
|
-
if (resolved.hostSource
|
|
1535
|
-
|
|
1536
|
-
`This repo's config (${resolved.repoConfigPath ?? ".harness-cli/config.json"}) targets ${resolved.baseUrl} \u2014 log in there? [y/N] `
|
|
1537
|
-
);
|
|
1538
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
1539
|
-
process.stderr.write(
|
|
1540
|
-
`harness: not logging in to ${resolved.baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
1541
|
-
`
|
|
1542
|
-
);
|
|
1543
|
-
return exitWith(2);
|
|
1544
|
-
}
|
|
1545
|
-
trustHost(resolved.baseUrl);
|
|
1781
|
+
if (!await confirmRepoLogin(resolved.baseUrl, resolved.hostSource, resolved.repoConfigPath)) {
|
|
1782
|
+
return exitWith(2);
|
|
1546
1783
|
}
|
|
1547
|
-
const loginResult = await login(resolved.baseUrl, {});
|
|
1784
|
+
const loginResult = await login(resolved.baseUrl, resolved.org === void 0 ? {} : { org: resolved.org });
|
|
1548
1785
|
if (!loginResult.ok) {
|
|
1549
|
-
|
|
1786
|
+
writeStderr(`harness: ${clean(loginResult.message)}
|
|
1550
1787
|
`);
|
|
1551
1788
|
return exitWith(1);
|
|
1552
1789
|
}
|
|
1553
|
-
|
|
1790
|
+
writeStderr(loginResult.human);
|
|
1554
1791
|
const again = resolveConfig();
|
|
1555
1792
|
if (again.kind !== "ok") {
|
|
1556
|
-
|
|
1793
|
+
writeStderr(
|
|
1557
1794
|
`harness: still not signed in to ${resolved.baseUrl} after logging in
|
|
1558
1795
|
`
|
|
1559
1796
|
);
|
|
1560
1797
|
return exitWith(1);
|
|
1561
1798
|
}
|
|
1562
|
-
return execute(again.config, jsonFlag, mutating,
|
|
1799
|
+
return execute(again.config, jsonFlag, mutating, run2);
|
|
1563
1800
|
}
|
|
1564
|
-
return execute(resolved.config, jsonFlag, mutating,
|
|
1801
|
+
return execute(resolved.config, jsonFlag, mutating, run2);
|
|
1565
1802
|
}
|
|
1566
|
-
async function execute(config, jsonFlag, mutating,
|
|
1803
|
+
async function execute(config, jsonFlag, mutating, run2) {
|
|
1567
1804
|
const client = new Client(config);
|
|
1568
1805
|
const hostNotice = mutating ? `\u2192 ${config.baseUrl}
|
|
1569
1806
|
` : "";
|
|
1570
1807
|
try {
|
|
1571
|
-
const output = await
|
|
1572
|
-
|
|
1573
|
-
if (output.notice !== void 0)
|
|
1808
|
+
const output = await run2({ client, config });
|
|
1809
|
+
writeStderr(hostNotice);
|
|
1810
|
+
if (output.notice !== void 0) writeStderr(output.notice);
|
|
1574
1811
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1575
1812
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1576
1813
|
} catch (error) {
|
|
@@ -1579,16 +1816,16 @@ async function execute(config, jsonFlag, mutating, run) {
|
|
|
1579
1816
|
}
|
|
1580
1817
|
async function fail(error, hostNotice) {
|
|
1581
1818
|
if (error instanceof UsageError) {
|
|
1582
|
-
|
|
1819
|
+
writeStderr(error.message + "\n");
|
|
1583
1820
|
return exitWith(2);
|
|
1584
1821
|
}
|
|
1585
1822
|
if (error instanceof LocalError) {
|
|
1586
|
-
|
|
1823
|
+
writeStderr(`harness: ${clean(error.message)}
|
|
1587
1824
|
`);
|
|
1588
1825
|
return exitWith(1);
|
|
1589
1826
|
}
|
|
1590
1827
|
if (error instanceof TransportError) {
|
|
1591
|
-
|
|
1828
|
+
writeStderr(
|
|
1592
1829
|
hostNotice + `harness: cannot reach the API: ${clean(error.message)}
|
|
1593
1830
|
`
|
|
1594
1831
|
);
|
|
@@ -1598,21 +1835,21 @@ async function fail(error, hostNotice) {
|
|
|
1598
1835
|
const healed = selfHeal();
|
|
1599
1836
|
if (healed.kind === "upgraded") return exitWith(healed.code);
|
|
1600
1837
|
const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
|
|
1601
|
-
|
|
1838
|
+
writeStderr(
|
|
1602
1839
|
hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
|
|
1603
1840
|
`
|
|
1604
1841
|
);
|
|
1605
1842
|
return exitWith(1);
|
|
1606
1843
|
}
|
|
1607
1844
|
if (error instanceof HttpError) {
|
|
1608
|
-
|
|
1845
|
+
writeStderr(
|
|
1609
1846
|
hostNotice + `harness: ${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}
|
|
1610
1847
|
`
|
|
1611
1848
|
);
|
|
1612
1849
|
return exitWith(1);
|
|
1613
1850
|
}
|
|
1614
1851
|
if (error instanceof CommandError) {
|
|
1615
|
-
|
|
1852
|
+
writeStderr(hostNotice + `harness: ${clean(error.message)}
|
|
1616
1853
|
`);
|
|
1617
1854
|
return exitWith(1);
|
|
1618
1855
|
}
|
|
@@ -1880,32 +2117,37 @@ plan.command("create").description("create a plan from a JSON or YAML file (the
|
|
|
1880
2117
|
return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
|
|
1881
2118
|
});
|
|
1882
2119
|
});
|
|
1883
|
-
plan.command("
|
|
2120
|
+
plan.command("list").description("a team's plans, newest first: root, status, held, revision and title").option("--team <key>", "the team whose issues root the plans (or HARNESS_TEAM)").option("--status <status>", PLAN_STATUSES.join(" | ")).option("--limit <n>", `maximum plans (default 50, at most ${PLAN_LIST_MAX_LIMIT}; there are no pages)`, integer).option("--json", "emit JSON").action(async (opts) => {
|
|
2121
|
+
requirePlanListArgs(opts);
|
|
2122
|
+
await withConfig(opts.json === true, false, (ctx) => planList(ctx, opts));
|
|
2123
|
+
});
|
|
2124
|
+
var PLAN_ARGUMENT = "the plan's id, or its root issue's identifier (e.g. FR-1)";
|
|
2125
|
+
plan.command("show").description("a plan's status, budgets, revision, steps, edges and wait conditions").argument("[plan]", PLAN_ARGUMENT).option("--json", "emit JSON").action(async (id, opts) => {
|
|
1884
2126
|
await withConfig(opts.json === true, false, (ctx) => {
|
|
1885
|
-
if (id === void 0) throw new UsageError("usage: harness plan show <plan-id>");
|
|
2127
|
+
if (id === void 0) throw new UsageError("usage: harness plan show <plan-id-or-root>");
|
|
1886
2128
|
return planShow(ctx, id);
|
|
1887
2129
|
});
|
|
1888
2130
|
});
|
|
1889
|
-
plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[
|
|
2131
|
+
plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[plan]", PLAN_ARGUMENT).option("--file <path>", 'the edit: .json is JSON, anything else YAML ("-" for stdin)').option("--json", "emit JSON").action(async (id, opts) => {
|
|
1890
2132
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1891
2133
|
if (id === void 0 || opts.file === void 0) {
|
|
1892
|
-
throw new UsageError("usage: harness plan edit <plan-id> --file <edits.json|edits.yaml>");
|
|
2134
|
+
throw new UsageError("usage: harness plan edit <plan-id-or-root> --file <edits.json|edits.yaml>");
|
|
1893
2135
|
}
|
|
1894
|
-
|
|
2136
|
+
requirePlanRef(id);
|
|
1895
2137
|
return planEdit(ctx, id, readPlanFile(opts.file));
|
|
1896
2138
|
});
|
|
1897
2139
|
});
|
|
1898
|
-
plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[
|
|
2140
|
+
plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[plan]", PLAN_ARGUMENT).option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer).option("--json", "emit JSON").action(
|
|
1899
2141
|
async (id, opts) => {
|
|
1900
2142
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1901
|
-
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id>");
|
|
2143
|
+
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id-or-root>");
|
|
1902
2144
|
return planRelease(ctx, id, { ...opts, collaborators: opts.collaborator });
|
|
1903
2145
|
});
|
|
1904
2146
|
}
|
|
1905
2147
|
);
|
|
1906
|
-
plan.command("hold").description("pause every step of a plan until it is released again").argument("[
|
|
2148
|
+
plan.command("hold").description("pause every step of a plan until it is released again").argument("[plan]", PLAN_ARGUMENT).option("--json", "emit JSON").action(async (id, opts) => {
|
|
1907
2149
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1908
|
-
if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id>");
|
|
2150
|
+
if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id-or-root>");
|
|
1909
2151
|
return planHold(ctx, id);
|
|
1910
2152
|
});
|
|
1911
2153
|
});
|
|
@@ -2040,6 +2282,32 @@ ${NOT_RETRIED}`
|
|
|
2040
2282
|
await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
|
|
2041
2283
|
});
|
|
2042
2284
|
var NOT_IDEMPOTENT = "Not idempotent: it is sent once and never retried, not after a timeout or a 5xx either. Exit 3 means the request never reached the server, so nothing changed. When it may have, the outcome is unknown: it exits 1 and says so. Then check";
|
|
2285
|
+
program.command("run").description("start an Operator job from an outcome, as Run as job does in the app; --watch follows it").argument("[outcome...]").option("--budget <usd>", "US dollars the organization's monthly Operator budget must still have (admins only)", once("--budget")).option("--skill <name>", "run one of the organization's Operator skills, by invocation name (/name)", once("--skill")).option("--watch", "follow the job like `watch --through-waits`; it never approves").option("--json", "emit JSON").addHelpText(
|
|
2286
|
+
"after",
|
|
2287
|
+
`
|
|
2288
|
+
Starts a job for you in a new org chat of yours, under your own current rights.
|
|
2289
|
+
Prints the job id, its state and its workbench link, and the approval link
|
|
2290
|
+
while a change set waits for approval. Approving happens in the app; the CLI
|
|
2291
|
+
never approves. You can start 30 jobs an hour. ${NOT_IDEMPOTENT} \`jobs --mine\` for the new job before running it again.
|
|
2292
|
+
|
|
2293
|
+
With --watch the job is followed until it completes (exit 0) or fails or is
|
|
2294
|
+
cancelled (exit ${EXIT_JOB_ENDED}). When it waits for a person, the approval link is printed and the
|
|
2295
|
+
watch keeps waiting; Ctrl-C stops it (exit ${EXIT_INTERRUPTED}) and stderr says how to resume.
|
|
2296
|
+
`
|
|
2297
|
+
).action(async (words, opts) => {
|
|
2298
|
+
const request = prepareRun(words.join(" "), { budget: opts.budget, skill: opts.skill });
|
|
2299
|
+
const json2 = opts.json === true;
|
|
2300
|
+
await withConfig(
|
|
2301
|
+
json2,
|
|
2302
|
+
true,
|
|
2303
|
+
(ctx) => run(ctx, request, {
|
|
2304
|
+
watch: opts.watch === true,
|
|
2305
|
+
json: json2,
|
|
2306
|
+
onLine: (line) => process.stdout.write(line),
|
|
2307
|
+
signal: opts.watch === true ? watchStopSignal() : void 0
|
|
2308
|
+
})
|
|
2309
|
+
);
|
|
2310
|
+
});
|
|
2043
2311
|
function once(flag) {
|
|
2044
2312
|
return (value, previous) => {
|
|
2045
2313
|
if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
|
|
@@ -2275,15 +2543,15 @@ link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json",
|
|
|
2275
2543
|
});
|
|
2276
2544
|
}
|
|
2277
2545
|
);
|
|
2278
|
-
var
|
|
2279
|
-
|
|
2546
|
+
var label2 = program.command("label").description("labels");
|
|
2547
|
+
label2.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
2280
2548
|
await withConfig(
|
|
2281
2549
|
opts.json === true,
|
|
2282
2550
|
false,
|
|
2283
2551
|
(ctx) => labelList(ctx)
|
|
2284
2552
|
);
|
|
2285
2553
|
});
|
|
2286
|
-
|
|
2554
|
+
label2.command("create").argument("[name]").option("--color <color>", "label color").option("--json", "emit JSON").action(
|
|
2287
2555
|
async (name, opts) => {
|
|
2288
2556
|
await withConfig(
|
|
2289
2557
|
opts.json === true,
|
|
@@ -2292,10 +2560,10 @@ label.command("create").argument("[name]").option("--color <color>", "label colo
|
|
|
2292
2560
|
);
|
|
2293
2561
|
}
|
|
2294
2562
|
);
|
|
2295
|
-
|
|
2563
|
+
label2.command("update").description("rename or recolor a label").argument("[id]").option("--name <name>", "new name").option("--color <color>", "swatch name (red, blue, \u2026) or a hex code").option("--json", "emit JSON").action(async (id, opts) => {
|
|
2296
2564
|
await withConfig(opts.json === true, true, (ctx) => labelUpdate(ctx, id, opts));
|
|
2297
2565
|
});
|
|
2298
|
-
|
|
2566
|
+
label2.command("delete").description("delete a label (refused while issues carry it)").argument("[id]").option("--yes", "confirm the deletion").option("--json", "emit JSON").action(async (id, opts) => {
|
|
2299
2567
|
await withConfig(opts.json === true, true, (ctx) => labelDelete(ctx, id, opts));
|
|
2300
2568
|
});
|
|
2301
2569
|
var team = program.command("team").description("teams");
|
|
@@ -2406,7 +2674,7 @@ program.command("skills").description("the organization's skills assigned to Ope
|
|
|
2406
2674
|
await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
|
|
2407
2675
|
});
|
|
2408
2676
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2409
|
-
const { startMcpServer } = await import("./server-
|
|
2677
|
+
const { startMcpServer } = await import("./server-F6YRKNC2.js");
|
|
2410
2678
|
await startMcpServer(invokedAs);
|
|
2411
2679
|
});
|
|
2412
2680
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -2428,6 +2696,17 @@ keys.command("exec").description(
|
|
|
2428
2696
|
(ctx) => exec(ctx, { command, override: opts.override === true, only })
|
|
2429
2697
|
);
|
|
2430
2698
|
});
|
|
2699
|
+
var review = program.command("review").description("a pull request's enforced Review Council gate, and a retry of its latest attempt");
|
|
2700
|
+
review.command("status").description("the gate's enforced and advisory status, and a failed latest attempt with what comes next").argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--json", "emit JSON").action(async (pr, opts) => {
|
|
2701
|
+
const ref = parsePullRequest(pr, opts.repo);
|
|
2702
|
+
await withConfig(opts.json === true, false, (ctx) => status(ctx, ref));
|
|
2703
|
+
});
|
|
2704
|
+
review.command("retry").description(
|
|
2705
|
+
"retry the latest gate attempt of the pull request's current head (needs reviews:write); the head, check and attempt are read from the gate status"
|
|
2706
|
+
).argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--reason <text>", "why the attempt is retried, recorded with it").option("--json", "emit JSON").action(async (pr, opts) => {
|
|
2707
|
+
const ref = parsePullRequest(pr, opts.repo);
|
|
2708
|
+
await withConfig(opts.json === true, true, (ctx) => retry(ctx, ref, opts.reason));
|
|
2709
|
+
});
|
|
2431
2710
|
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(
|
|
2432
2711
|
"--timeout <ms>",
|
|
2433
2712
|
"how long to wait for the browser callback",
|
|
@@ -2436,34 +2715,52 @@ program.command("login").description("sign in via the browser (infra.one session
|
|
|
2436
2715
|
async (opts) => {
|
|
2437
2716
|
const repoLookup = findRepoConfig();
|
|
2438
2717
|
for (const warning of repoLookup.warnings)
|
|
2439
|
-
|
|
2718
|
+
writeStderr(warning + "\n");
|
|
2440
2719
|
const repoUrl = repoLookup.config?.url;
|
|
2441
|
-
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
|
|
2720
|
+
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? (repoLookup.config?.org === void 0 ? storedLoginUrl() : void 0) ?? DEFAULT_BASE_URL;
|
|
2442
2721
|
if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
|
|
2443
|
-
|
|
2722
|
+
writeStderr(
|
|
2444
2723
|
`harness: host ${url} comes from ${repoLookup.config?.path}
|
|
2445
2724
|
`
|
|
2446
2725
|
);
|
|
2447
2726
|
}
|
|
2727
|
+
const repoSelected = opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0 && repoUrl !== void 0;
|
|
2728
|
+
if (repoSelected && !isHostTrusted(url)) {
|
|
2729
|
+
if (!canConfirmRepoLogin()) {
|
|
2730
|
+
writeStderr("harness: cannot confirm a repo-selected host without a terminal \u2014 pass --url explicitly\n");
|
|
2731
|
+
return exitWith(2);
|
|
2732
|
+
}
|
|
2733
|
+
if (!await confirmRepoLogin(url, "repo-config", repoLookup.config?.path)) return exitWith(2);
|
|
2734
|
+
}
|
|
2448
2735
|
const options = { noBrowser: !opts.browser };
|
|
2736
|
+
if (repoLookup.config?.org !== void 0) options.org = repoLookup.config.org;
|
|
2449
2737
|
if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
|
|
2450
2738
|
await finishAuth(await login(url, options), opts.json === true);
|
|
2451
2739
|
}
|
|
2452
2740
|
);
|
|
2453
2741
|
program.command("logout").description("revoke and forget the stored login").option("--json", "emit JSON").action(async (opts) => {
|
|
2454
|
-
|
|
2742
|
+
const selected = resolveConfig({ ...process.env, HARNESS_API_TOKEN: void 0 });
|
|
2743
|
+
if (selected.kind === "error") {
|
|
2744
|
+
writeStderr(`harness: ${selected.message}
|
|
2745
|
+
`);
|
|
2746
|
+
return exitWith(2);
|
|
2747
|
+
}
|
|
2748
|
+
const path = selected.kind === "ok" ? selected.config.credentialsPath : selected.credentialsPath;
|
|
2749
|
+
await finishAuth(await logout(path === void 0 ? {} : { credentialsPath: path }), opts.json === true);
|
|
2455
2750
|
});
|
|
2456
2751
|
program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
|
|
2457
2752
|
const resolved = resolveConfig();
|
|
2458
2753
|
for (const warning of resolved.warnings)
|
|
2459
|
-
|
|
2754
|
+
writeStderr(warning + "\n");
|
|
2460
2755
|
if (resolved.kind === "error") {
|
|
2461
|
-
|
|
2756
|
+
writeStderr(`harness: ${resolved.message}
|
|
2462
2757
|
`);
|
|
2463
2758
|
return exitWith(2);
|
|
2464
2759
|
}
|
|
2465
2760
|
if (resolved.kind === "needs-login") {
|
|
2466
|
-
|
|
2761
|
+
writeStderr(loginHelp(resolved.baseUrl));
|
|
2762
|
+
if (resolved.org !== void 0) writeStderr(`Choose organization ${resolved.org} when logging in.
|
|
2763
|
+
`);
|
|
2467
2764
|
return exitWith(2);
|
|
2468
2765
|
}
|
|
2469
2766
|
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
@@ -2472,7 +2769,7 @@ program.command("whoami").description("show the resolved host and credential sou
|
|
|
2472
2769
|
result = await whoami(resolved.config, source);
|
|
2473
2770
|
} catch (error) {
|
|
2474
2771
|
if (!(error instanceof TransportError)) throw error;
|
|
2475
|
-
|
|
2772
|
+
writeStderr(`harness: cannot reach the API: ${clean(error.message)}
|
|
2476
2773
|
`);
|
|
2477
2774
|
return exitWith(3);
|
|
2478
2775
|
}
|
|
@@ -2489,7 +2786,7 @@ async function finishAuth(result, json2) {
|
|
|
2489
2786
|
process.stdout.write(json2 ? json(result.json) : result.human);
|
|
2490
2787
|
return exitWith(0);
|
|
2491
2788
|
}
|
|
2492
|
-
|
|
2789
|
+
writeStderr(`harness: ${clean(result.message)}
|
|
2493
2790
|
`);
|
|
2494
2791
|
return exitWith(1);
|
|
2495
2792
|
}
|
|
@@ -2501,7 +2798,7 @@ if (argv[2] === UPDATE_CHECK_COMMAND) {
|
|
|
2501
2798
|
}
|
|
2502
2799
|
try {
|
|
2503
2800
|
const notice = updateCheck();
|
|
2504
|
-
if (notice !== null)
|
|
2801
|
+
if (notice !== null) writeStderr(notice);
|
|
2505
2802
|
} catch {
|
|
2506
2803
|
}
|
|
2507
2804
|
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
@@ -2513,7 +2810,7 @@ try {
|
|
|
2513
2810
|
await program.parseAsync(argv);
|
|
2514
2811
|
} catch (error) {
|
|
2515
2812
|
if (error instanceof UsageError) {
|
|
2516
|
-
|
|
2813
|
+
writeStderr(error.message + "\n");
|
|
2517
2814
|
await exitWith(2);
|
|
2518
2815
|
}
|
|
2519
2816
|
if (error instanceof CommanderError) {
|