@allocator-one/harness-cli 1.15.0 → 1.15.1
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 +53 -11
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-I2S2JVCQ.js → chunk-L2OWOZ4N.js} +240 -63
- package/dist/cli.js +185 -113
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-O7CX2ZCC.js → server-KVRQBVXI.js} +57 -12
- package/package.json +1 -1
- package/spec/openapi.json +1098 -293
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,
|
|
@@ -18,6 +16,7 @@ import {
|
|
|
18
16
|
LocalError,
|
|
19
17
|
RELATION_CREATE_TYPES,
|
|
20
18
|
RELATION_UPDATE_TYPES,
|
|
19
|
+
SESSION_PATH,
|
|
21
20
|
STEER_MAX_CHARACTERS,
|
|
22
21
|
TransportError,
|
|
23
22
|
UpgradeRequiredError,
|
|
@@ -50,6 +49,7 @@ import {
|
|
|
50
49
|
instructionFrom,
|
|
51
50
|
isExpertToken,
|
|
52
51
|
isHostTrusted,
|
|
52
|
+
isLoginCredential,
|
|
53
53
|
jobCancel,
|
|
54
54
|
jobContinue,
|
|
55
55
|
jobPause,
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
planRelease,
|
|
80
80
|
planShow,
|
|
81
81
|
prepareProcedureRun,
|
|
82
|
+
prepareRun,
|
|
82
83
|
prime,
|
|
83
84
|
procedureSave,
|
|
84
85
|
procedureShow,
|
|
@@ -110,6 +111,7 @@ import {
|
|
|
110
111
|
resolveCacheHome,
|
|
111
112
|
resolveConfig,
|
|
112
113
|
resolveTextSources,
|
|
114
|
+
run,
|
|
113
115
|
search,
|
|
114
116
|
show,
|
|
115
117
|
skillsList,
|
|
@@ -137,7 +139,14 @@ import {
|
|
|
137
139
|
watchStopSignal,
|
|
138
140
|
workflowStates,
|
|
139
141
|
writeCredentials
|
|
140
|
-
} from "./chunk-
|
|
142
|
+
} from "./chunk-L2OWOZ4N.js";
|
|
143
|
+
import {
|
|
144
|
+
CLI_VERSION,
|
|
145
|
+
OPERATOR_PACKAGE,
|
|
146
|
+
invokedName,
|
|
147
|
+
layoutOperatorHelp,
|
|
148
|
+
named
|
|
149
|
+
} from "./chunk-FQWV5MIC.js";
|
|
141
150
|
|
|
142
151
|
// src/cli.ts
|
|
143
152
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -267,8 +276,10 @@ async function exec(ctx, opts) {
|
|
|
267
276
|
const missing = unstoredSlugs(requested, stored);
|
|
268
277
|
if (missing.length > 0) {
|
|
269
278
|
process.stderr.write(
|
|
270
|
-
|
|
279
|
+
named(
|
|
280
|
+
`harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
|
|
271
281
|
`
|
|
282
|
+
)
|
|
272
283
|
);
|
|
273
284
|
}
|
|
274
285
|
}
|
|
@@ -338,9 +349,10 @@ var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
|
|
|
338
349
|
var MAX_PAGE_RESPONSES = 4;
|
|
339
350
|
var loginPageImport;
|
|
340
351
|
function loadLoginPage() {
|
|
341
|
-
loginPageImport ??= import("./login-page-
|
|
352
|
+
loginPageImport ??= import("./login-page-KGEJFFXR.js");
|
|
342
353
|
return loginPageImport;
|
|
343
354
|
}
|
|
355
|
+
var NO_SESSION = { kind: null, expires_at: null, scopes: null, user: null, org: null };
|
|
344
356
|
function generateState() {
|
|
345
357
|
return randomBytes(24).toString("base64url");
|
|
346
358
|
}
|
|
@@ -439,7 +451,7 @@ async function loopbackLogin(baseUrl, options) {
|
|
|
439
451
|
);
|
|
440
452
|
return token;
|
|
441
453
|
}
|
|
442
|
-
const result = await verifyAndStore(baseUrl, token.token, options);
|
|
454
|
+
const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
|
|
443
455
|
if (result.ok) {
|
|
444
456
|
await callback.respond(200, "You can close this browser and return to the terminal.", "success");
|
|
445
457
|
} else {
|
|
@@ -492,7 +504,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
492
504
|
if (phase !== "waiting") return;
|
|
493
505
|
const timeoutResponse = {
|
|
494
506
|
status: 410,
|
|
495
|
-
message:
|
|
507
|
+
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
496
508
|
pageState: "error"
|
|
497
509
|
};
|
|
498
510
|
phase = "terminal";
|
|
@@ -564,7 +576,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
564
576
|
}
|
|
565
577
|
const result = finalResponse ?? {
|
|
566
578
|
status: 410,
|
|
567
|
-
message:
|
|
579
|
+
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
568
580
|
pageState: "error"
|
|
569
581
|
};
|
|
570
582
|
void trackResponse(respondWithPage(result.status, result.message, result.pageState));
|
|
@@ -696,7 +708,8 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
696
708
|
try {
|
|
697
709
|
const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
|
|
698
710
|
const token = body.data?.token;
|
|
699
|
-
|
|
711
|
+
const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
|
|
712
|
+
if (typeof token === "string" && token !== "") return { ok: true, token, kind };
|
|
700
713
|
return {
|
|
701
714
|
ok: false,
|
|
702
715
|
message: "the login-code exchange returned no token \u2014 aborting"
|
|
@@ -725,20 +738,28 @@ async function manualLogin(baseUrl, options) {
|
|
|
725
738
|
);
|
|
726
739
|
const token = (await prompt()).trim();
|
|
727
740
|
if (token === "") return { ok: false, message: "no token entered" };
|
|
728
|
-
return verifyAndStore(baseUrl, token, options);
|
|
741
|
+
return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
|
|
729
742
|
}
|
|
730
|
-
async function verifyAndStore(baseUrl, token, options) {
|
|
743
|
+
async function verifyAndStore(baseUrl, token, options, received) {
|
|
731
744
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
732
745
|
const client = bareClient(baseUrl, token, options);
|
|
733
746
|
const expert = isExpertToken(token);
|
|
747
|
+
let kind = received.kind;
|
|
734
748
|
try {
|
|
735
|
-
|
|
749
|
+
if (expert) {
|
|
750
|
+
await client.get(EXPERT_ASSIGNMENTS_PATH);
|
|
751
|
+
} else {
|
|
752
|
+
const described = isLoginCredential(token) ? await describeSession(client) : null;
|
|
753
|
+
if (described === null) await client.get("/api/v1/teams");
|
|
754
|
+
else kind = described.session.kind ?? kind;
|
|
755
|
+
}
|
|
736
756
|
} catch (error) {
|
|
737
757
|
if (error instanceof HttpError) {
|
|
738
758
|
const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
|
|
759
|
+
const revoked = received.minted && await revokeSession(client, token);
|
|
739
760
|
return {
|
|
740
761
|
ok: false,
|
|
741
|
-
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${lapsed}`
|
|
762
|
+
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${revoked ? ", and the token was revoked" : ""}${lapsed}`
|
|
742
763
|
};
|
|
743
764
|
}
|
|
744
765
|
if (error instanceof TransportError) {
|
|
@@ -754,11 +775,11 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
754
775
|
try {
|
|
755
776
|
writeCredentials(path, baseUrl, token);
|
|
756
777
|
} catch (error) {
|
|
757
|
-
await revokeSession(client, token);
|
|
778
|
+
if (received.minted) await revokeSession(client, token);
|
|
758
779
|
const detail = error instanceof Error ? error.message : String(error);
|
|
759
780
|
return {
|
|
760
781
|
ok: false,
|
|
761
|
-
message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : "")
|
|
782
|
+
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
783
|
};
|
|
763
784
|
}
|
|
764
785
|
let supersededNote = "";
|
|
@@ -768,12 +789,40 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
768
789
|
supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
|
|
769
790
|
}
|
|
770
791
|
}
|
|
792
|
+
const name = invokedName();
|
|
771
793
|
return {
|
|
772
794
|
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
|
|
795
|
+
json: { url: baseUrl, credentials: path, kind },
|
|
796
|
+
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.
|
|
797
|
+
` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; ${name} logout revokes it).
|
|
798
|
+
`) + kindNote(kind) + supersededNote
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function kindNote(kind) {
|
|
802
|
+
if (kind === "operator-cli") {
|
|
803
|
+
return "This credential is Operator-only (operator-cli): Operator commands work; issue, plan and gate commands are refused.\n";
|
|
804
|
+
}
|
|
805
|
+
if (kind === "harness-cli") return "This credential has full access (harness-cli).\n";
|
|
806
|
+
return "";
|
|
807
|
+
}
|
|
808
|
+
async function describeSession(client) {
|
|
809
|
+
try {
|
|
810
|
+
const { envelope, apiVersion } = await client.getVersioned(SESSION_PATH);
|
|
811
|
+
return { session: sessionOf(envelope.data), apiVersion };
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error instanceof HttpError && (error.status === 404 || error.status === 405)) return null;
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function sessionOf(data) {
|
|
818
|
+
if (!isRecord2(data)) return NO_SESSION;
|
|
819
|
+
const text = (value) => typeof value === "string" ? value : null;
|
|
820
|
+
return {
|
|
821
|
+
kind: text(data["kind"]),
|
|
822
|
+
expires_at: text(data["expires_at"]),
|
|
823
|
+
scopes: Array.isArray(data["scopes"]) ? data["scopes"].filter((scope) => typeof scope === "string") : null,
|
|
824
|
+
user: isRecord2(data["user"]) ? data["user"] : null,
|
|
825
|
+
org: isRecord2(data["org"]) ? data["org"] : null
|
|
777
826
|
};
|
|
778
827
|
}
|
|
779
828
|
async function revokeSession(client, token) {
|
|
@@ -814,8 +863,18 @@ async function whoami(config, credentialSource) {
|
|
|
814
863
|
const client = new Client(config);
|
|
815
864
|
const expert = isExpertToken(config.token);
|
|
816
865
|
let probe;
|
|
866
|
+
let session = null;
|
|
867
|
+
let serverVersion;
|
|
817
868
|
try {
|
|
818
|
-
|
|
869
|
+
const described2 = isLoginCredential(config.token) ? await describeSession(client) : null;
|
|
870
|
+
if (described2 !== null) {
|
|
871
|
+
session = described2.session;
|
|
872
|
+
serverVersion = described2.apiVersion;
|
|
873
|
+
} else {
|
|
874
|
+
const answered = await client.getVersioned(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats");
|
|
875
|
+
probe = answered.envelope.data;
|
|
876
|
+
serverVersion = answered.apiVersion;
|
|
877
|
+
}
|
|
819
878
|
} catch (error) {
|
|
820
879
|
if (error instanceof HttpError) {
|
|
821
880
|
return {
|
|
@@ -835,7 +894,7 @@ async function whoami(config, credentialSource) {
|
|
|
835
894
|
};
|
|
836
895
|
}
|
|
837
896
|
}
|
|
838
|
-
|
|
897
|
+
serverVersion ??= "unknown";
|
|
839
898
|
const hostSourceLabels = {
|
|
840
899
|
env: "HARNESS_API_URL",
|
|
841
900
|
"repo-config": ".harness-cli/config.json",
|
|
@@ -846,6 +905,7 @@ async function whoami(config, credentialSource) {
|
|
|
846
905
|
const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
|
|
847
906
|
Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
|
|
848
907
|
`;
|
|
908
|
+
const described = session ?? NO_SESSION;
|
|
849
909
|
return {
|
|
850
910
|
ok: true,
|
|
851
911
|
json: {
|
|
@@ -854,14 +914,48 @@ Reaches: its own assignments and the gates it owns on them (assignments, con
|
|
|
854
914
|
verified: true,
|
|
855
915
|
server_version: serverVersion,
|
|
856
916
|
host_source: config.hostSource,
|
|
917
|
+
kind: described.kind,
|
|
918
|
+
expires_at: described.expires_at,
|
|
919
|
+
scopes: described.scopes,
|
|
920
|
+
user: described.user,
|
|
921
|
+
org: described.org,
|
|
857
922
|
expert: seat
|
|
858
923
|
},
|
|
859
924
|
human: `Host: ${config.baseUrl}${via}
|
|
860
|
-
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}
|
|
861
|
-
` + expertLines + `Verified: ok (server API ${serverVersion})
|
|
925
|
+
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}${kindLabel(described.kind)}
|
|
926
|
+
` + sessionLines(described) + expertLines + `Verified: ok (server API ${serverVersion})
|
|
862
927
|
`
|
|
863
928
|
};
|
|
864
929
|
}
|
|
930
|
+
function kindLabel(kind) {
|
|
931
|
+
if (kind === null) return "";
|
|
932
|
+
if (kind === "harness-cli") return ", harness-cli (full access)";
|
|
933
|
+
if (kind === "operator-cli") return ", operator-cli (Operator only)";
|
|
934
|
+
return `, ${clean(kind)}`;
|
|
935
|
+
}
|
|
936
|
+
function sessionLines(session) {
|
|
937
|
+
let lines = "";
|
|
938
|
+
const user2 = session.user;
|
|
939
|
+
if (user2 !== null) {
|
|
940
|
+
const name = clean(user2["name"]);
|
|
941
|
+
const email = clean(user2["email"]);
|
|
942
|
+
lines += `User: ${name === "" ? email : email === "" ? name : `${name} <${email}>`}
|
|
943
|
+
`;
|
|
944
|
+
}
|
|
945
|
+
const org = session.org;
|
|
946
|
+
if (org !== null) {
|
|
947
|
+
const name = clean(org["name"]);
|
|
948
|
+
const slug = clean(org["slug"]);
|
|
949
|
+
lines += `Org: ${slug === "" || slug === name ? name : `${name} (${slug})`}
|
|
950
|
+
`;
|
|
951
|
+
}
|
|
952
|
+
if (session.expires_at !== null) lines += `Expires: ${clean(session.expires_at)}
|
|
953
|
+
`;
|
|
954
|
+
if (session.kind === "operator-cli") {
|
|
955
|
+
lines += "Reaches: Operator commands only; issue, plan and gate commands are refused\n";
|
|
956
|
+
}
|
|
957
|
+
return lines;
|
|
958
|
+
}
|
|
865
959
|
function expertSeat(data) {
|
|
866
960
|
if (!isRecord2(data) || !isRecord2(data["expert"])) return null;
|
|
867
961
|
const plan2 = isRecord2(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
|
|
@@ -883,7 +977,7 @@ function bareClient(baseUrl, token, options) {
|
|
|
883
977
|
return new Client(config);
|
|
884
978
|
}
|
|
885
979
|
function defaultNotify(line) {
|
|
886
|
-
process.stderr.write(line + "\n");
|
|
980
|
+
process.stderr.write(named(line + "\n"));
|
|
887
981
|
}
|
|
888
982
|
function defaultPrompt() {
|
|
889
983
|
const readline = createInterface({
|
|
@@ -914,57 +1008,6 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
|
914
1008
|
}
|
|
915
1009
|
}
|
|
916
1010
|
|
|
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
1011
|
// src/core/upgrade.ts
|
|
969
1012
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
970
1013
|
import {
|
|
@@ -980,7 +1023,7 @@ import {
|
|
|
980
1023
|
statSync,
|
|
981
1024
|
writeFileSync
|
|
982
1025
|
} from "fs";
|
|
983
|
-
import { basename
|
|
1026
|
+
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
984
1027
|
import { fileURLToPath } from "url";
|
|
985
1028
|
var PACKAGE_NAME = "@allocator-one/harness-cli";
|
|
986
1029
|
var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
|
|
@@ -1053,7 +1096,7 @@ function enclosingNodeModules(packageRoot) {
|
|
|
1053
1096
|
let dir = dirname(packageRoot);
|
|
1054
1097
|
const { root } = parse(dir);
|
|
1055
1098
|
for (; ; ) {
|
|
1056
|
-
if (
|
|
1099
|
+
if (basename(dir) === "node_modules") return dir;
|
|
1057
1100
|
if (dir === root) return null;
|
|
1058
1101
|
dir = dirname(dir);
|
|
1059
1102
|
}
|
|
@@ -1299,8 +1342,8 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1299
1342
|
};
|
|
1300
1343
|
}
|
|
1301
1344
|
process.stderr.write(
|
|
1302
|
-
`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1303
|
-
`
|
|
1345
|
+
named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1346
|
+
`)
|
|
1304
1347
|
);
|
|
1305
1348
|
const args = [
|
|
1306
1349
|
"install",
|
|
@@ -1368,8 +1411,8 @@ function reexecSelf(env, argv2) {
|
|
|
1368
1411
|
});
|
|
1369
1412
|
if (result.error !== void 0) {
|
|
1370
1413
|
process.stderr.write(
|
|
1371
|
-
`harness: upgraded, but re-running failed: ${result.error.message}
|
|
1372
|
-
`
|
|
1414
|
+
named(`harness: upgraded, but re-running failed: ${result.error.message}
|
|
1415
|
+
`)
|
|
1373
1416
|
);
|
|
1374
1417
|
return 1;
|
|
1375
1418
|
}
|
|
@@ -1473,11 +1516,14 @@ function exitWith(code) {
|
|
|
1473
1516
|
guardBrokenPipes();
|
|
1474
1517
|
var program = new Command();
|
|
1475
1518
|
var invokedAs = invokedName();
|
|
1519
|
+
function writeStderr(text) {
|
|
1520
|
+
return process.stderr.write(named(text, invokedAs));
|
|
1521
|
+
}
|
|
1476
1522
|
program.name(invokedAs).description(
|
|
1477
1523
|
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
1524
|
).exitOverride().configureOutput({
|
|
1479
|
-
writeOut: (text) => process.stdout.write(text),
|
|
1480
|
-
writeErr: (text) =>
|
|
1525
|
+
writeOut: (text) => process.stdout.write(named(text, invokedAs)),
|
|
1526
|
+
writeErr: (text) => writeStderr(text)
|
|
1481
1527
|
});
|
|
1482
1528
|
function integer(value) {
|
|
1483
1529
|
if (!/^\d+$/.test(value))
|
|
@@ -1518,17 +1564,17 @@ function interactive() {
|
|
|
1518
1564
|
const ci = process.env["CI"];
|
|
1519
1565
|
return process.stdout.isTTY === true && (ci === void 0 || ci === "");
|
|
1520
1566
|
}
|
|
1521
|
-
async function withConfig(jsonFlag, mutating,
|
|
1567
|
+
async function withConfig(jsonFlag, mutating, run2) {
|
|
1522
1568
|
const resolved = resolveConfig();
|
|
1523
|
-
for (const warning of resolved.warnings)
|
|
1569
|
+
for (const warning of resolved.warnings) writeStderr(warning + "\n");
|
|
1524
1570
|
if (resolved.kind === "error") {
|
|
1525
|
-
|
|
1571
|
+
writeStderr(`harness: ${resolved.message}
|
|
1526
1572
|
`);
|
|
1527
1573
|
return exitWith(2);
|
|
1528
1574
|
}
|
|
1529
1575
|
if (resolved.kind === "needs-login") {
|
|
1530
1576
|
if (!interactive()) {
|
|
1531
|
-
|
|
1577
|
+
writeStderr(loginHelp(resolved.baseUrl));
|
|
1532
1578
|
return exitWith(2);
|
|
1533
1579
|
}
|
|
1534
1580
|
if (resolved.hostSource === "repo-config" && !isHostTrusted(resolved.baseUrl)) {
|
|
@@ -1536,7 +1582,7 @@ async function withConfig(jsonFlag, mutating, run) {
|
|
|
1536
1582
|
`This repo's config (${resolved.repoConfigPath ?? ".harness-cli/config.json"}) targets ${resolved.baseUrl} \u2014 log in there? [y/N] `
|
|
1537
1583
|
);
|
|
1538
1584
|
if (answer.trim().toLowerCase() !== "y") {
|
|
1539
|
-
|
|
1585
|
+
writeStderr(
|
|
1540
1586
|
`harness: not logging in to ${resolved.baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
1541
1587
|
`
|
|
1542
1588
|
);
|
|
@@ -1546,31 +1592,31 @@ async function withConfig(jsonFlag, mutating, run) {
|
|
|
1546
1592
|
}
|
|
1547
1593
|
const loginResult = await login(resolved.baseUrl, {});
|
|
1548
1594
|
if (!loginResult.ok) {
|
|
1549
|
-
|
|
1595
|
+
writeStderr(`harness: ${clean(loginResult.message)}
|
|
1550
1596
|
`);
|
|
1551
1597
|
return exitWith(1);
|
|
1552
1598
|
}
|
|
1553
|
-
|
|
1599
|
+
writeStderr(loginResult.human);
|
|
1554
1600
|
const again = resolveConfig();
|
|
1555
1601
|
if (again.kind !== "ok") {
|
|
1556
|
-
|
|
1602
|
+
writeStderr(
|
|
1557
1603
|
`harness: still not signed in to ${resolved.baseUrl} after logging in
|
|
1558
1604
|
`
|
|
1559
1605
|
);
|
|
1560
1606
|
return exitWith(1);
|
|
1561
1607
|
}
|
|
1562
|
-
return execute(again.config, jsonFlag, mutating,
|
|
1608
|
+
return execute(again.config, jsonFlag, mutating, run2);
|
|
1563
1609
|
}
|
|
1564
|
-
return execute(resolved.config, jsonFlag, mutating,
|
|
1610
|
+
return execute(resolved.config, jsonFlag, mutating, run2);
|
|
1565
1611
|
}
|
|
1566
|
-
async function execute(config, jsonFlag, mutating,
|
|
1612
|
+
async function execute(config, jsonFlag, mutating, run2) {
|
|
1567
1613
|
const client = new Client(config);
|
|
1568
1614
|
const hostNotice = mutating ? `\u2192 ${config.baseUrl}
|
|
1569
1615
|
` : "";
|
|
1570
1616
|
try {
|
|
1571
|
-
const output = await
|
|
1572
|
-
|
|
1573
|
-
if (output.notice !== void 0)
|
|
1617
|
+
const output = await run2({ client, config });
|
|
1618
|
+
writeStderr(hostNotice);
|
|
1619
|
+
if (output.notice !== void 0) writeStderr(output.notice);
|
|
1574
1620
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1575
1621
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1576
1622
|
} catch (error) {
|
|
@@ -1579,16 +1625,16 @@ async function execute(config, jsonFlag, mutating, run) {
|
|
|
1579
1625
|
}
|
|
1580
1626
|
async function fail(error, hostNotice) {
|
|
1581
1627
|
if (error instanceof UsageError) {
|
|
1582
|
-
|
|
1628
|
+
writeStderr(error.message + "\n");
|
|
1583
1629
|
return exitWith(2);
|
|
1584
1630
|
}
|
|
1585
1631
|
if (error instanceof LocalError) {
|
|
1586
|
-
|
|
1632
|
+
writeStderr(`harness: ${clean(error.message)}
|
|
1587
1633
|
`);
|
|
1588
1634
|
return exitWith(1);
|
|
1589
1635
|
}
|
|
1590
1636
|
if (error instanceof TransportError) {
|
|
1591
|
-
|
|
1637
|
+
writeStderr(
|
|
1592
1638
|
hostNotice + `harness: cannot reach the API: ${clean(error.message)}
|
|
1593
1639
|
`
|
|
1594
1640
|
);
|
|
@@ -1598,21 +1644,21 @@ async function fail(error, hostNotice) {
|
|
|
1598
1644
|
const healed = selfHeal();
|
|
1599
1645
|
if (healed.kind === "upgraded") return exitWith(healed.code);
|
|
1600
1646
|
const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
|
|
1601
|
-
|
|
1647
|
+
writeStderr(
|
|
1602
1648
|
hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
|
|
1603
1649
|
`
|
|
1604
1650
|
);
|
|
1605
1651
|
return exitWith(1);
|
|
1606
1652
|
}
|
|
1607
1653
|
if (error instanceof HttpError) {
|
|
1608
|
-
|
|
1654
|
+
writeStderr(
|
|
1609
1655
|
hostNotice + `harness: ${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}
|
|
1610
1656
|
`
|
|
1611
1657
|
);
|
|
1612
1658
|
return exitWith(1);
|
|
1613
1659
|
}
|
|
1614
1660
|
if (error instanceof CommandError) {
|
|
1615
|
-
|
|
1661
|
+
writeStderr(hostNotice + `harness: ${clean(error.message)}
|
|
1616
1662
|
`);
|
|
1617
1663
|
return exitWith(1);
|
|
1618
1664
|
}
|
|
@@ -2040,6 +2086,32 @@ ${NOT_RETRIED}`
|
|
|
2040
2086
|
await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
|
|
2041
2087
|
});
|
|
2042
2088
|
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";
|
|
2089
|
+
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(
|
|
2090
|
+
"after",
|
|
2091
|
+
`
|
|
2092
|
+
Starts a job for you in a new org chat of yours, under your own current rights.
|
|
2093
|
+
Prints the job id, its state and its workbench link, and the approval link
|
|
2094
|
+
while a change set waits for approval. Approving happens in the app; the CLI
|
|
2095
|
+
never approves. You can start 30 jobs an hour. ${NOT_IDEMPOTENT} \`jobs --mine\` for the new job before running it again.
|
|
2096
|
+
|
|
2097
|
+
With --watch the job is followed until it completes (exit 0) or fails or is
|
|
2098
|
+
cancelled (exit ${EXIT_JOB_ENDED}). When it waits for a person, the approval link is printed and the
|
|
2099
|
+
watch keeps waiting; Ctrl-C stops it (exit ${EXIT_INTERRUPTED}) and stderr says how to resume.
|
|
2100
|
+
`
|
|
2101
|
+
).action(async (words, opts) => {
|
|
2102
|
+
const request = prepareRun(words.join(" "), { budget: opts.budget, skill: opts.skill });
|
|
2103
|
+
const json2 = opts.json === true;
|
|
2104
|
+
await withConfig(
|
|
2105
|
+
json2,
|
|
2106
|
+
true,
|
|
2107
|
+
(ctx) => run(ctx, request, {
|
|
2108
|
+
watch: opts.watch === true,
|
|
2109
|
+
json: json2,
|
|
2110
|
+
onLine: (line) => process.stdout.write(line),
|
|
2111
|
+
signal: opts.watch === true ? watchStopSignal() : void 0
|
|
2112
|
+
})
|
|
2113
|
+
);
|
|
2114
|
+
});
|
|
2043
2115
|
function once(flag) {
|
|
2044
2116
|
return (value, previous) => {
|
|
2045
2117
|
if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
|
|
@@ -2406,7 +2478,7 @@ program.command("skills").description("the organization's skills assigned to Ope
|
|
|
2406
2478
|
await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
|
|
2407
2479
|
});
|
|
2408
2480
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2409
|
-
const { startMcpServer } = await import("./server-
|
|
2481
|
+
const { startMcpServer } = await import("./server-KVRQBVXI.js");
|
|
2410
2482
|
await startMcpServer(invokedAs);
|
|
2411
2483
|
});
|
|
2412
2484
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -2436,11 +2508,11 @@ program.command("login").description("sign in via the browser (infra.one session
|
|
|
2436
2508
|
async (opts) => {
|
|
2437
2509
|
const repoLookup = findRepoConfig();
|
|
2438
2510
|
for (const warning of repoLookup.warnings)
|
|
2439
|
-
|
|
2511
|
+
writeStderr(warning + "\n");
|
|
2440
2512
|
const repoUrl = repoLookup.config?.url;
|
|
2441
2513
|
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
|
|
2442
2514
|
if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
|
|
2443
|
-
|
|
2515
|
+
writeStderr(
|
|
2444
2516
|
`harness: host ${url} comes from ${repoLookup.config?.path}
|
|
2445
2517
|
`
|
|
2446
2518
|
);
|
|
@@ -2456,14 +2528,14 @@ program.command("logout").description("revoke and forget the stored login").opti
|
|
|
2456
2528
|
program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
|
|
2457
2529
|
const resolved = resolveConfig();
|
|
2458
2530
|
for (const warning of resolved.warnings)
|
|
2459
|
-
|
|
2531
|
+
writeStderr(warning + "\n");
|
|
2460
2532
|
if (resolved.kind === "error") {
|
|
2461
|
-
|
|
2533
|
+
writeStderr(`harness: ${resolved.message}
|
|
2462
2534
|
`);
|
|
2463
2535
|
return exitWith(2);
|
|
2464
2536
|
}
|
|
2465
2537
|
if (resolved.kind === "needs-login") {
|
|
2466
|
-
|
|
2538
|
+
writeStderr(loginHelp(resolved.baseUrl));
|
|
2467
2539
|
return exitWith(2);
|
|
2468
2540
|
}
|
|
2469
2541
|
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
@@ -2472,7 +2544,7 @@ program.command("whoami").description("show the resolved host and credential sou
|
|
|
2472
2544
|
result = await whoami(resolved.config, source);
|
|
2473
2545
|
} catch (error) {
|
|
2474
2546
|
if (!(error instanceof TransportError)) throw error;
|
|
2475
|
-
|
|
2547
|
+
writeStderr(`harness: cannot reach the API: ${clean(error.message)}
|
|
2476
2548
|
`);
|
|
2477
2549
|
return exitWith(3);
|
|
2478
2550
|
}
|
|
@@ -2489,7 +2561,7 @@ async function finishAuth(result, json2) {
|
|
|
2489
2561
|
process.stdout.write(json2 ? json(result.json) : result.human);
|
|
2490
2562
|
return exitWith(0);
|
|
2491
2563
|
}
|
|
2492
|
-
|
|
2564
|
+
writeStderr(`harness: ${clean(result.message)}
|
|
2493
2565
|
`);
|
|
2494
2566
|
return exitWith(1);
|
|
2495
2567
|
}
|
|
@@ -2501,7 +2573,7 @@ if (argv[2] === UPDATE_CHECK_COMMAND) {
|
|
|
2501
2573
|
}
|
|
2502
2574
|
try {
|
|
2503
2575
|
const notice = updateCheck();
|
|
2504
|
-
if (notice !== null)
|
|
2576
|
+
if (notice !== null) writeStderr(notice);
|
|
2505
2577
|
} catch {
|
|
2506
2578
|
}
|
|
2507
2579
|
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
@@ -2513,7 +2585,7 @@ try {
|
|
|
2513
2585
|
await program.parseAsync(argv);
|
|
2514
2586
|
} catch (error) {
|
|
2515
2587
|
if (error instanceof UsageError) {
|
|
2516
|
-
|
|
2588
|
+
writeStderr(error.message + "\n");
|
|
2517
2589
|
await exitWith(2);
|
|
2518
2590
|
}
|
|
2519
2591
|
if (error instanceof CommanderError) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
invokedName,
|
|
4
|
+
named
|
|
5
|
+
} from "./chunk-FQWV5MIC.js";
|
|
2
6
|
|
|
3
7
|
// src/core/login-page.ts
|
|
4
8
|
import { createHash } from "crypto";
|
|
@@ -157,8 +161,10 @@ var TERMINAL_SCRIPT = `(function () {
|
|
|
157
161
|
|
|
158
162
|
void play();
|
|
159
163
|
})();`;
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
function terminalScript(name) {
|
|
165
|
+
const script = named(TERMINAL_SCRIPT, name).replace(/\r\n/g, "\n");
|
|
166
|
+
return { script, hash: createHash("sha256").update(script).digest("base64") };
|
|
167
|
+
}
|
|
162
168
|
var PRESENTATION = {
|
|
163
169
|
success: {
|
|
164
170
|
eyebrow: "Login complete",
|
|
@@ -176,7 +182,8 @@ var PRESENTATION = {
|
|
|
176
182
|
function escapeHtml(text) {
|
|
177
183
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
178
184
|
}
|
|
179
|
-
function loginPage(message, state) {
|
|
185
|
+
function loginPage(message, state, name = invokedName()) {
|
|
186
|
+
const { script, hash } = terminalScript(name);
|
|
180
187
|
const resolvedState = Object.hasOwn(PRESENTATION, state) ? state : "waiting";
|
|
181
188
|
const { eyebrow, heading } = PRESENTATION[resolvedState];
|
|
182
189
|
const safeEyebrow = escapeHtml(eyebrow);
|
|
@@ -187,7 +194,7 @@ function loginPage(message, state) {
|
|
|
187
194
|
<html lang="en"><head>
|
|
188
195
|
<meta charset="utf-8">
|
|
189
196
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
190
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src data:; img-src data:; script-src 'sha256-${
|
|
197
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src data:; img-src data:; script-src 'sha256-${hash}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
|
191
198
|
<meta name="color-scheme" content="light">
|
|
192
199
|
<title>${safeHeading} \xB7 Harness</title>
|
|
193
200
|
<style>
|
|
@@ -281,7 +288,7 @@ function loginPage(message, state) {
|
|
|
281
288
|
<div class="terminal-stream" data-terminal-stream aria-hidden="true"></div>
|
|
282
289
|
</div>
|
|
283
290
|
</div>
|
|
284
|
-
<script>${
|
|
291
|
+
<script>${script}</script>` : ""}
|
|
285
292
|
</aside>
|
|
286
293
|
</main>
|
|
287
294
|
</body></html>
|