@allocator-one/harness-cli 1.14.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 +585 -26
- package/dist/chunk-FQWV5MIC.js +73 -0
- package/dist/{chunk-F2YVTB2A.js → chunk-L2OWOZ4N.js} +2362 -288
- package/dist/cli.js +533 -93
- package/dist/{login-page-UVUKFFTT.js → login-page-KGEJFFXR.js} +12 -5
- package/dist/{server-75QJASTY.js → server-KVRQBVXI.js} +264 -24
- package/package.json +2 -1
- package/spec/openapi.json +13100 -3920
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ACTIVITY_MAX_LIMIT,
|
|
4
|
-
CLI_VERSION,
|
|
5
4
|
Client,
|
|
6
5
|
CommandError,
|
|
7
6
|
DEFAULT_BASE_URL,
|
|
7
|
+
EXIT_INTERRUPTED,
|
|
8
|
+
EXIT_JOB_ENDED,
|
|
9
|
+
EXIT_WAITING_FOR_PERSON,
|
|
10
|
+
EXPERT_ASSIGNMENTS_PATH,
|
|
11
|
+
EXPERT_LAPSED,
|
|
8
12
|
HttpError,
|
|
13
|
+
JOBS_MAX_LIMIT,
|
|
14
|
+
JOBS_MAX_PAGES,
|
|
15
|
+
JOB_STATES,
|
|
9
16
|
LocalError,
|
|
10
17
|
RELATION_CREATE_TYPES,
|
|
11
18
|
RELATION_UPDATE_TYPES,
|
|
19
|
+
SESSION_PATH,
|
|
20
|
+
STEER_MAX_CHARACTERS,
|
|
12
21
|
TransportError,
|
|
13
22
|
UpgradeRequiredError,
|
|
14
23
|
UsageError,
|
|
15
24
|
activity,
|
|
16
25
|
archive,
|
|
26
|
+
assignments,
|
|
17
27
|
attachImage,
|
|
18
28
|
blocked,
|
|
19
29
|
clean,
|
|
20
30
|
close,
|
|
21
31
|
comment,
|
|
22
32
|
comments,
|
|
33
|
+
conditionList,
|
|
23
34
|
create,
|
|
24
35
|
defaultCredentialsPath,
|
|
25
36
|
deleteComment,
|
|
@@ -31,9 +42,21 @@ import {
|
|
|
31
42
|
docUpdate,
|
|
32
43
|
editComment,
|
|
33
44
|
errorDetails,
|
|
45
|
+
expertSummary,
|
|
46
|
+
explainExpertRefusal,
|
|
34
47
|
findRepoConfig,
|
|
35
48
|
gateSet,
|
|
49
|
+
instructionFrom,
|
|
50
|
+
isExpertToken,
|
|
36
51
|
isHostTrusted,
|
|
52
|
+
isLoginCredential,
|
|
53
|
+
jobCancel,
|
|
54
|
+
jobContinue,
|
|
55
|
+
jobPause,
|
|
56
|
+
jobShow,
|
|
57
|
+
jobSteer,
|
|
58
|
+
jobWatch,
|
|
59
|
+
jobsList,
|
|
37
60
|
json,
|
|
38
61
|
labelCreate,
|
|
39
62
|
labelDelete,
|
|
@@ -55,7 +78,11 @@ import {
|
|
|
55
78
|
planHold,
|
|
56
79
|
planRelease,
|
|
57
80
|
planShow,
|
|
81
|
+
prepareProcedureRun,
|
|
82
|
+
prepareRun,
|
|
58
83
|
prime,
|
|
84
|
+
procedureSave,
|
|
85
|
+
procedureShow,
|
|
59
86
|
projectCreate,
|
|
60
87
|
projectList,
|
|
61
88
|
projectShow,
|
|
@@ -67,17 +94,28 @@ import {
|
|
|
67
94
|
relationList,
|
|
68
95
|
relationRm,
|
|
69
96
|
relationUpdate,
|
|
97
|
+
requireCapabilityId,
|
|
70
98
|
requireCloseArgs,
|
|
71
99
|
requireCommentId,
|
|
72
100
|
requireIdentifiers,
|
|
101
|
+
requireJobId,
|
|
102
|
+
requireJobsArgs,
|
|
73
103
|
requirePlanId,
|
|
104
|
+
requireProcedureId,
|
|
105
|
+
requireProcedureName,
|
|
74
106
|
requireReadyClaimArgs,
|
|
107
|
+
requireSchemaView,
|
|
108
|
+
requireSearchQuery,
|
|
109
|
+
requireToolsOpts,
|
|
75
110
|
requiredTeam,
|
|
76
111
|
resolveCacheHome,
|
|
77
112
|
resolveConfig,
|
|
78
113
|
resolveTextSources,
|
|
114
|
+
run,
|
|
79
115
|
search,
|
|
80
116
|
show,
|
|
117
|
+
skillsList,
|
|
118
|
+
startProcedureRun,
|
|
81
119
|
stateAdd,
|
|
82
120
|
stateDelete,
|
|
83
121
|
stateOrder,
|
|
@@ -87,6 +125,10 @@ import {
|
|
|
87
125
|
teamList,
|
|
88
126
|
teamRename,
|
|
89
127
|
teamSetDefault,
|
|
128
|
+
toolShow,
|
|
129
|
+
toolsDomains,
|
|
130
|
+
toolsList,
|
|
131
|
+
toolsSearch,
|
|
90
132
|
trustHost,
|
|
91
133
|
update,
|
|
92
134
|
upload,
|
|
@@ -94,9 +136,17 @@ import {
|
|
|
94
136
|
validateBaseUrl,
|
|
95
137
|
validateIdempotencyKey,
|
|
96
138
|
version,
|
|
139
|
+
watchStopSignal,
|
|
97
140
|
workflowStates,
|
|
98
141
|
writeCredentials
|
|
99
|
-
} 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";
|
|
100
150
|
|
|
101
151
|
// src/cli.ts
|
|
102
152
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -226,8 +276,10 @@ async function exec(ctx, opts) {
|
|
|
226
276
|
const missing = unstoredSlugs(requested, stored);
|
|
227
277
|
if (missing.length > 0) {
|
|
228
278
|
process.stderr.write(
|
|
229
|
-
|
|
279
|
+
named(
|
|
280
|
+
`harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
|
|
230
281
|
`
|
|
282
|
+
)
|
|
231
283
|
);
|
|
232
284
|
}
|
|
233
285
|
}
|
|
@@ -297,9 +349,10 @@ var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
|
|
|
297
349
|
var MAX_PAGE_RESPONSES = 4;
|
|
298
350
|
var loginPageImport;
|
|
299
351
|
function loadLoginPage() {
|
|
300
|
-
loginPageImport ??= import("./login-page-
|
|
352
|
+
loginPageImport ??= import("./login-page-KGEJFFXR.js");
|
|
301
353
|
return loginPageImport;
|
|
302
354
|
}
|
|
355
|
+
var NO_SESSION = { kind: null, expires_at: null, scopes: null, user: null, org: null };
|
|
303
356
|
function generateState() {
|
|
304
357
|
return randomBytes(24).toString("base64url");
|
|
305
358
|
}
|
|
@@ -398,7 +451,7 @@ async function loopbackLogin(baseUrl, options) {
|
|
|
398
451
|
);
|
|
399
452
|
return token;
|
|
400
453
|
}
|
|
401
|
-
const result = await verifyAndStore(baseUrl, token.token, options);
|
|
454
|
+
const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
|
|
402
455
|
if (result.ok) {
|
|
403
456
|
await callback.respond(200, "You can close this browser and return to the terminal.", "success");
|
|
404
457
|
} else {
|
|
@@ -451,7 +504,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
451
504
|
if (phase !== "waiting") return;
|
|
452
505
|
const timeoutResponse = {
|
|
453
506
|
status: 410,
|
|
454
|
-
message:
|
|
507
|
+
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
455
508
|
pageState: "error"
|
|
456
509
|
};
|
|
457
510
|
phase = "terminal";
|
|
@@ -523,7 +576,7 @@ function awaitCallback(server, state2, timeoutMs) {
|
|
|
523
576
|
}
|
|
524
577
|
const result = finalResponse ?? {
|
|
525
578
|
status: 410,
|
|
526
|
-
message:
|
|
579
|
+
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
527
580
|
pageState: "error"
|
|
528
581
|
};
|
|
529
582
|
void trackResponse(respondWithPage(result.status, result.message, result.pageState));
|
|
@@ -655,7 +708,8 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
655
708
|
try {
|
|
656
709
|
const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
|
|
657
710
|
const token = body.data?.token;
|
|
658
|
-
|
|
711
|
+
const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
|
|
712
|
+
if (typeof token === "string" && token !== "") return { ok: true, token, kind };
|
|
659
713
|
return {
|
|
660
714
|
ok: false,
|
|
661
715
|
message: "the login-code exchange returned no token \u2014 aborting"
|
|
@@ -679,21 +733,33 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
679
733
|
async function manualLogin(baseUrl, options) {
|
|
680
734
|
const notify = options.notify ?? defaultNotify;
|
|
681
735
|
const prompt = options.prompt ?? defaultPrompt;
|
|
682
|
-
notify(
|
|
736
|
+
notify(
|
|
737
|
+
`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).`
|
|
738
|
+
);
|
|
683
739
|
const token = (await prompt()).trim();
|
|
684
740
|
if (token === "") return { ok: false, message: "no token entered" };
|
|
685
|
-
return verifyAndStore(baseUrl, token, options);
|
|
741
|
+
return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
|
|
686
742
|
}
|
|
687
|
-
async function verifyAndStore(baseUrl, token, options) {
|
|
743
|
+
async function verifyAndStore(baseUrl, token, options, received) {
|
|
688
744
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
689
745
|
const client = bareClient(baseUrl, token, options);
|
|
746
|
+
const expert = isExpertToken(token);
|
|
747
|
+
let kind = received.kind;
|
|
690
748
|
try {
|
|
691
|
-
|
|
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
|
+
}
|
|
692
756
|
} catch (error) {
|
|
693
757
|
if (error instanceof HttpError) {
|
|
758
|
+
const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
|
|
759
|
+
const revoked = received.minted && await revokeSession(client, token);
|
|
694
760
|
return {
|
|
695
761
|
ok: false,
|
|
696
|
-
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored`
|
|
762
|
+
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${revoked ? ", and the token was revoked" : ""}${lapsed}`
|
|
697
763
|
};
|
|
698
764
|
}
|
|
699
765
|
if (error instanceof TransportError) {
|
|
@@ -709,33 +775,72 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
709
775
|
try {
|
|
710
776
|
writeCredentials(path, baseUrl, token);
|
|
711
777
|
} catch (error) {
|
|
712
|
-
|
|
778
|
+
if (received.minted) await revokeSession(client, token);
|
|
713
779
|
const detail = error instanceof Error ? error.message : String(error);
|
|
714
780
|
return {
|
|
715
781
|
ok: false,
|
|
716
|
-
message: `could not store credentials at ${path}: ${detail}`
|
|
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)")
|
|
717
783
|
};
|
|
718
784
|
}
|
|
785
|
+
let supersededNote = "";
|
|
719
786
|
if (superseded !== null) {
|
|
720
|
-
await bareClient(superseded.url, superseded.token, options).
|
|
787
|
+
await revokeSession(bareClient(superseded.url, superseded.token, options), superseded.token);
|
|
788
|
+
if (isExpertToken(superseded.token)) {
|
|
789
|
+
supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
|
|
790
|
+
}
|
|
721
791
|
}
|
|
792
|
+
const name = invokedName();
|
|
722
793
|
return {
|
|
723
794
|
ok: true,
|
|
724
|
-
json: { url: baseUrl, credentials: path },
|
|
725
|
-
human: `
|
|
726
|
-
`
|
|
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
|
|
727
826
|
};
|
|
728
827
|
}
|
|
828
|
+
async function revokeSession(client, token) {
|
|
829
|
+
if (isExpertToken(token)) return false;
|
|
830
|
+
return client.delete("/api/v1/session").then(() => true).catch(() => false);
|
|
831
|
+
}
|
|
729
832
|
async function logout(options = {}) {
|
|
730
833
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
731
834
|
const stored = readCredentials(path);
|
|
732
835
|
if (stored === null) {
|
|
836
|
+
const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
|
|
733
837
|
return {
|
|
734
838
|
ok: false,
|
|
735
|
-
message: "No stored login to remove. A HARNESS_API_TOKEN from the environment is yours to manage \u2014 unset it, or disable the token in the admin UI."
|
|
839
|
+
message: "No stored login to remove. A HARNESS_API_TOKEN from the environment is yours to manage \u2014 " + (isExpertToken(envToken.trim()) ? "unset it, or revoke the pxc_ credential in your expert view." : "unset it, or disable the token in the admin UI.")
|
|
736
840
|
};
|
|
737
841
|
}
|
|
738
|
-
const
|
|
842
|
+
const expert = isExpertToken(stored.token);
|
|
843
|
+
const revoked = await revokeSession(bareClient(stored.url, stored.token, options), stored.token);
|
|
739
844
|
const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
|
|
740
845
|
try {
|
|
741
846
|
deleteCredentials(path);
|
|
@@ -749,24 +854,47 @@ async function logout(options = {}) {
|
|
|
749
854
|
return {
|
|
750
855
|
ok: true,
|
|
751
856
|
json: { logged_out: true, revoked },
|
|
752
|
-
human: `
|
|
857
|
+
human: expert ? `Forgot the outside expert's pxc_ credential for ${stored.url}. It stays valid until it expires: revoke it in your expert view to end it now.
|
|
858
|
+
` : `Logged out of ${stored.url}${note}.
|
|
753
859
|
`
|
|
754
860
|
};
|
|
755
861
|
}
|
|
756
862
|
async function whoami(config, credentialSource) {
|
|
757
863
|
const client = new Client(config);
|
|
864
|
+
const expert = isExpertToken(config.token);
|
|
865
|
+
let probe;
|
|
866
|
+
let session = null;
|
|
867
|
+
let serverVersion;
|
|
758
868
|
try {
|
|
759
|
-
await client
|
|
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
|
+
}
|
|
760
878
|
} catch (error) {
|
|
761
879
|
if (error instanceof HttpError) {
|
|
762
880
|
return {
|
|
763
881
|
ok: false,
|
|
764
|
-
message: `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
|
|
882
|
+
message: expert ? `signed in to ${config.baseUrl} with an outside expert's pxc_ credential (${credentialSource}), but it was refused: ${error.message} (HTTP ${error.status})${error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : ""}` : `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
|
|
765
883
|
};
|
|
766
884
|
}
|
|
767
885
|
throw error;
|
|
768
886
|
}
|
|
769
|
-
|
|
887
|
+
let seat = null;
|
|
888
|
+
if (expert) {
|
|
889
|
+
seat = expertSeat(probe);
|
|
890
|
+
if (seat === null) {
|
|
891
|
+
return {
|
|
892
|
+
ok: false,
|
|
893
|
+
message: `signed in to ${config.baseUrl} with an outside expert's pxc_ credential, but the assignments response carried no expert`
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
serverVersion ??= "unknown";
|
|
770
898
|
const hostSourceLabels = {
|
|
771
899
|
env: "HARNESS_API_URL",
|
|
772
900
|
"repo-config": ".harness-cli/config.json",
|
|
@@ -774,6 +902,10 @@ async function whoami(config, credentialSource) {
|
|
|
774
902
|
default: "workspace default"
|
|
775
903
|
};
|
|
776
904
|
const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
|
|
905
|
+
const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
|
|
906
|
+
Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
|
|
907
|
+
`;
|
|
908
|
+
const described = session ?? NO_SESSION;
|
|
777
909
|
return {
|
|
778
910
|
ok: true,
|
|
779
911
|
json: {
|
|
@@ -781,14 +913,58 @@ async function whoami(config, credentialSource) {
|
|
|
781
913
|
source: credentialSource,
|
|
782
914
|
verified: true,
|
|
783
915
|
server_version: serverVersion,
|
|
784
|
-
host_source: config.hostSource
|
|
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,
|
|
922
|
+
expert: seat
|
|
785
923
|
},
|
|
786
924
|
human: `Host: ${config.baseUrl}${via}
|
|
787
|
-
Credential: ${credentialSource}
|
|
788
|
-
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})
|
|
789
927
|
`
|
|
790
928
|
};
|
|
791
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
|
+
}
|
|
959
|
+
function expertSeat(data) {
|
|
960
|
+
if (!isRecord2(data) || !isRecord2(data["expert"])) return null;
|
|
961
|
+
const plan2 = isRecord2(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
|
|
962
|
+
const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord2).length : 0;
|
|
963
|
+
return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
|
|
964
|
+
}
|
|
965
|
+
function isRecord2(value) {
|
|
966
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
967
|
+
}
|
|
792
968
|
function bareClient(baseUrl, token, options) {
|
|
793
969
|
const config = {
|
|
794
970
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
@@ -801,7 +977,7 @@ function bareClient(baseUrl, token, options) {
|
|
|
801
977
|
return new Client(config);
|
|
802
978
|
}
|
|
803
979
|
function defaultNotify(line) {
|
|
804
|
-
process.stderr.write(line + "\n");
|
|
980
|
+
process.stderr.write(named(line + "\n"));
|
|
805
981
|
}
|
|
806
982
|
function defaultPrompt() {
|
|
807
983
|
const readline = createInterface({
|
|
@@ -850,10 +1026,16 @@ import {
|
|
|
850
1026
|
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
851
1027
|
import { fileURLToPath } from "url";
|
|
852
1028
|
var PACKAGE_NAME = "@allocator-one/harness-cli";
|
|
1029
|
+
var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
|
|
853
1030
|
var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
|
|
1031
|
+
function upgradeInstallSpec(install) {
|
|
1032
|
+
return `${install.packageName ?? PACKAGE_NAME}@latest`;
|
|
1033
|
+
}
|
|
854
1034
|
var UPDATE_CHECK_COMMAND = "__update-check";
|
|
855
1035
|
var REEXEC_SENTINEL = "HARNESS_UPGRADED";
|
|
856
|
-
|
|
1036
|
+
function latestUrl(packageName = PACKAGE_NAME) {
|
|
1037
|
+
return `https://registry.npmjs.org/${packageName}/latest`;
|
|
1038
|
+
}
|
|
857
1039
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
858
1040
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
859
1041
|
var NPM_ROOT_TIMEOUT_MS = 15e3;
|
|
@@ -866,21 +1048,24 @@ function detectInstall(from = fileURLToPath(import.meta.url)) {
|
|
|
866
1048
|
nodeModules: null
|
|
867
1049
|
};
|
|
868
1050
|
if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
|
|
869
|
-
const
|
|
870
|
-
if (
|
|
1051
|
+
const found = findPackageRoot(from);
|
|
1052
|
+
if (found === null) return unknown;
|
|
1053
|
+
const { packageRoot, packageName } = found;
|
|
871
1054
|
if (existsSync(join2(packageRoot, ".git"))) {
|
|
872
|
-
return { kind: "checkout", packageRoot, nodeModules: null };
|
|
1055
|
+
return { kind: "checkout", packageRoot, packageName, nodeModules: null };
|
|
873
1056
|
}
|
|
874
1057
|
const nodeModules = enclosingNodeModules(packageRoot);
|
|
875
|
-
if (nodeModules === null) return { ...unknown, packageRoot };
|
|
1058
|
+
if (nodeModules === null) return { ...unknown, packageRoot, packageName };
|
|
876
1059
|
const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
|
|
877
|
-
return { kind, packageRoot, nodeModules };
|
|
1060
|
+
return { kind, packageRoot, packageName, nodeModules };
|
|
878
1061
|
}
|
|
879
1062
|
function findPackageRoot(from) {
|
|
880
1063
|
let dir = dirname(from);
|
|
881
1064
|
const { root } = parse(dir);
|
|
882
1065
|
for (; ; ) {
|
|
883
|
-
|
|
1066
|
+
const name = packageNameAt(join2(dir, "package.json"));
|
|
1067
|
+
if (name !== null && PACKAGE_NAMES.includes(name))
|
|
1068
|
+
return { packageRoot: dir, packageName: name };
|
|
884
1069
|
if (dir === root) return null;
|
|
885
1070
|
dir = dirname(dir);
|
|
886
1071
|
}
|
|
@@ -958,12 +1143,15 @@ function comparePrerelease(left, right) {
|
|
|
958
1143
|
}
|
|
959
1144
|
return 0;
|
|
960
1145
|
}
|
|
961
|
-
function stampPath(env = process.env) {
|
|
962
|
-
|
|
1146
|
+
function stampPath(env = process.env, packageName = PACKAGE_NAME) {
|
|
1147
|
+
const file = packageName === PACKAGE_NAME ? "update-check.json" : `update-check-${packageName.replace(/^@[^/]+\//, "")}.json`;
|
|
1148
|
+
return join2(resolveCacheHome(env), "harness", file);
|
|
963
1149
|
}
|
|
964
|
-
function readStamp(env = process.env) {
|
|
1150
|
+
function readStamp(env = process.env, packageName = PACKAGE_NAME) {
|
|
965
1151
|
try {
|
|
966
|
-
const parsed = JSON.parse(
|
|
1152
|
+
const parsed = JSON.parse(
|
|
1153
|
+
readFileSync(stampPath(env, packageName), "utf8")
|
|
1154
|
+
);
|
|
967
1155
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
968
1156
|
const record = parsed;
|
|
969
1157
|
const checkedAt = record["checkedAt"];
|
|
@@ -975,10 +1163,10 @@ function readStamp(env = process.env) {
|
|
|
975
1163
|
return null;
|
|
976
1164
|
}
|
|
977
1165
|
}
|
|
978
|
-
function writeStamp(stamp, env = process.env) {
|
|
979
|
-
const path = stampPath(env);
|
|
1166
|
+
function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
|
|
1167
|
+
const path = stampPath(env, packageName);
|
|
980
1168
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
981
|
-
const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
|
|
1169
|
+
const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
|
|
982
1170
|
try {
|
|
983
1171
|
mkdirSync(dirname(path), { recursive: true });
|
|
984
1172
|
writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
|
|
@@ -996,9 +1184,10 @@ function updateCheck(options = {}) {
|
|
|
996
1184
|
if (!checksEnabled(env)) return null;
|
|
997
1185
|
const install = options.install ?? detectInstall();
|
|
998
1186
|
if (install.kind !== "global" && install.kind !== "local") return null;
|
|
999
|
-
const
|
|
1187
|
+
const packageName = install.packageName ?? PACKAGE_NAME;
|
|
1188
|
+
const stamp = readStamp(env, packageName);
|
|
1000
1189
|
if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
|
|
1001
|
-
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
|
|
1190
|
+
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
|
|
1002
1191
|
(options.refresh ?? spawnRefresh)();
|
|
1003
1192
|
}
|
|
1004
1193
|
if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
|
|
@@ -1008,7 +1197,7 @@ function updateCheck(options = {}) {
|
|
|
1008
1197
|
return null;
|
|
1009
1198
|
}
|
|
1010
1199
|
function upgradeHint(install) {
|
|
1011
|
-
return install.kind === "local" ? `npm i ${
|
|
1200
|
+
return install.kind === "local" ? `npm i ${upgradeInstallSpec(install)}` : `npm i -g ${upgradeInstallSpec(install)}`;
|
|
1012
1201
|
}
|
|
1013
1202
|
function spawnRefresh() {
|
|
1014
1203
|
const self = process.argv[1];
|
|
@@ -1024,13 +1213,17 @@ function spawnRefresh() {
|
|
|
1024
1213
|
} catch {
|
|
1025
1214
|
}
|
|
1026
1215
|
}
|
|
1027
|
-
async function performUpdateCheck(env = process.env, now = Date.now()) {
|
|
1216
|
+
async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
|
|
1028
1217
|
if (!checksEnabled(env)) return;
|
|
1029
|
-
writeStamp(
|
|
1218
|
+
writeStamp(
|
|
1219
|
+
{ checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
|
|
1220
|
+
env,
|
|
1221
|
+
packageName
|
|
1222
|
+
);
|
|
1030
1223
|
}
|
|
1031
|
-
async function fetchLatestVersion(env) {
|
|
1224
|
+
async function fetchLatestVersion(env, packageName) {
|
|
1032
1225
|
const configured = env["HARNESS_REGISTRY_URL"];
|
|
1033
|
-
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() :
|
|
1226
|
+
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
|
|
1034
1227
|
try {
|
|
1035
1228
|
const response = await fetch(url, {
|
|
1036
1229
|
headers: { accept: "application/json" },
|
|
@@ -1148,8 +1341,10 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1148
1341
|
detail: "lost the upgrade lock while preparing \u2014 another process took over"
|
|
1149
1342
|
};
|
|
1150
1343
|
}
|
|
1151
|
-
process.stderr.write(
|
|
1152
|
-
`
|
|
1344
|
+
process.stderr.write(
|
|
1345
|
+
named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1346
|
+
`)
|
|
1347
|
+
);
|
|
1153
1348
|
const args = [
|
|
1154
1349
|
"install",
|
|
1155
1350
|
"--global",
|
|
@@ -1158,7 +1353,7 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1158
1353
|
"--no-fund",
|
|
1159
1354
|
"--no-update-notifier",
|
|
1160
1355
|
"--loglevel=error",
|
|
1161
|
-
|
|
1356
|
+
upgradeInstallSpec(install)
|
|
1162
1357
|
];
|
|
1163
1358
|
const result = spawnSync(npmCommand(), args, {
|
|
1164
1359
|
// npm's diagnostics belong on stderr with ours; stdout stays the
|
|
@@ -1216,8 +1411,8 @@ function reexecSelf(env, argv2) {
|
|
|
1216
1411
|
});
|
|
1217
1412
|
if (result.error !== void 0) {
|
|
1218
1413
|
process.stderr.write(
|
|
1219
|
-
`harness: upgraded, but re-running failed: ${result.error.message}
|
|
1220
|
-
`
|
|
1414
|
+
named(`harness: upgraded, but re-running failed: ${result.error.message}
|
|
1415
|
+
`)
|
|
1221
1416
|
);
|
|
1222
1417
|
return 1;
|
|
1223
1418
|
}
|
|
@@ -1320,9 +1515,15 @@ function exitWith(code) {
|
|
|
1320
1515
|
}
|
|
1321
1516
|
guardBrokenPipes();
|
|
1322
1517
|
var program = new Command();
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1518
|
+
var invokedAs = invokedName();
|
|
1519
|
+
function writeStderr(text) {
|
|
1520
|
+
return process.stderr.write(named(text, invokedAs));
|
|
1521
|
+
}
|
|
1522
|
+
program.name(invokedAs).description(
|
|
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)"
|
|
1524
|
+
).exitOverride().configureOutput({
|
|
1525
|
+
writeOut: (text) => process.stdout.write(named(text, invokedAs)),
|
|
1526
|
+
writeErr: (text) => writeStderr(text)
|
|
1326
1527
|
});
|
|
1327
1528
|
function integer(value) {
|
|
1328
1529
|
if (!/^\d+$/.test(value))
|
|
@@ -1363,17 +1564,17 @@ function interactive() {
|
|
|
1363
1564
|
const ci = process.env["CI"];
|
|
1364
1565
|
return process.stdout.isTTY === true && (ci === void 0 || ci === "");
|
|
1365
1566
|
}
|
|
1366
|
-
async function withConfig(jsonFlag, mutating,
|
|
1567
|
+
async function withConfig(jsonFlag, mutating, run2) {
|
|
1367
1568
|
const resolved = resolveConfig();
|
|
1368
|
-
for (const warning of resolved.warnings)
|
|
1569
|
+
for (const warning of resolved.warnings) writeStderr(warning + "\n");
|
|
1369
1570
|
if (resolved.kind === "error") {
|
|
1370
|
-
|
|
1571
|
+
writeStderr(`harness: ${resolved.message}
|
|
1371
1572
|
`);
|
|
1372
1573
|
return exitWith(2);
|
|
1373
1574
|
}
|
|
1374
1575
|
if (resolved.kind === "needs-login") {
|
|
1375
1576
|
if (!interactive()) {
|
|
1376
|
-
|
|
1577
|
+
writeStderr(loginHelp(resolved.baseUrl));
|
|
1377
1578
|
return exitWith(2);
|
|
1378
1579
|
}
|
|
1379
1580
|
if (resolved.hostSource === "repo-config" && !isHostTrusted(resolved.baseUrl)) {
|
|
@@ -1381,7 +1582,7 @@ async function withConfig(jsonFlag, mutating, run) {
|
|
|
1381
1582
|
`This repo's config (${resolved.repoConfigPath ?? ".harness-cli/config.json"}) targets ${resolved.baseUrl} \u2014 log in there? [y/N] `
|
|
1382
1583
|
);
|
|
1383
1584
|
if (answer.trim().toLowerCase() !== "y") {
|
|
1384
|
-
|
|
1585
|
+
writeStderr(
|
|
1385
1586
|
`harness: not logging in to ${resolved.baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
1386
1587
|
`
|
|
1387
1588
|
);
|
|
@@ -1391,49 +1592,49 @@ async function withConfig(jsonFlag, mutating, run) {
|
|
|
1391
1592
|
}
|
|
1392
1593
|
const loginResult = await login(resolved.baseUrl, {});
|
|
1393
1594
|
if (!loginResult.ok) {
|
|
1394
|
-
|
|
1595
|
+
writeStderr(`harness: ${clean(loginResult.message)}
|
|
1395
1596
|
`);
|
|
1396
1597
|
return exitWith(1);
|
|
1397
1598
|
}
|
|
1398
|
-
|
|
1599
|
+
writeStderr(loginResult.human);
|
|
1399
1600
|
const again = resolveConfig();
|
|
1400
1601
|
if (again.kind !== "ok") {
|
|
1401
|
-
|
|
1602
|
+
writeStderr(
|
|
1402
1603
|
`harness: still not signed in to ${resolved.baseUrl} after logging in
|
|
1403
1604
|
`
|
|
1404
1605
|
);
|
|
1405
1606
|
return exitWith(1);
|
|
1406
1607
|
}
|
|
1407
|
-
return execute(again.config, jsonFlag, mutating,
|
|
1608
|
+
return execute(again.config, jsonFlag, mutating, run2);
|
|
1408
1609
|
}
|
|
1409
|
-
return execute(resolved.config, jsonFlag, mutating,
|
|
1610
|
+
return execute(resolved.config, jsonFlag, mutating, run2);
|
|
1410
1611
|
}
|
|
1411
|
-
async function execute(config, jsonFlag, mutating,
|
|
1612
|
+
async function execute(config, jsonFlag, mutating, run2) {
|
|
1412
1613
|
const client = new Client(config);
|
|
1413
1614
|
const hostNotice = mutating ? `\u2192 ${config.baseUrl}
|
|
1414
1615
|
` : "";
|
|
1415
1616
|
try {
|
|
1416
|
-
const output = await
|
|
1417
|
-
|
|
1418
|
-
if (output.notice !== void 0)
|
|
1617
|
+
const output = await run2({ client, config });
|
|
1618
|
+
writeStderr(hostNotice);
|
|
1619
|
+
if (output.notice !== void 0) writeStderr(output.notice);
|
|
1419
1620
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1420
1621
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1421
1622
|
} catch (error) {
|
|
1422
|
-
return fail(error, hostNotice);
|
|
1623
|
+
return fail(explainExpertRefusal(error, config.token), hostNotice);
|
|
1423
1624
|
}
|
|
1424
1625
|
}
|
|
1425
1626
|
async function fail(error, hostNotice) {
|
|
1426
1627
|
if (error instanceof UsageError) {
|
|
1427
|
-
|
|
1628
|
+
writeStderr(error.message + "\n");
|
|
1428
1629
|
return exitWith(2);
|
|
1429
1630
|
}
|
|
1430
1631
|
if (error instanceof LocalError) {
|
|
1431
|
-
|
|
1632
|
+
writeStderr(`harness: ${clean(error.message)}
|
|
1432
1633
|
`);
|
|
1433
1634
|
return exitWith(1);
|
|
1434
1635
|
}
|
|
1435
1636
|
if (error instanceof TransportError) {
|
|
1436
|
-
|
|
1637
|
+
writeStderr(
|
|
1437
1638
|
hostNotice + `harness: cannot reach the API: ${clean(error.message)}
|
|
1438
1639
|
`
|
|
1439
1640
|
);
|
|
@@ -1443,21 +1644,21 @@ async function fail(error, hostNotice) {
|
|
|
1443
1644
|
const healed = selfHeal();
|
|
1444
1645
|
if (healed.kind === "upgraded") return exitWith(healed.code);
|
|
1445
1646
|
const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
|
|
1446
|
-
|
|
1647
|
+
writeStderr(
|
|
1447
1648
|
hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
|
|
1448
1649
|
`
|
|
1449
1650
|
);
|
|
1450
1651
|
return exitWith(1);
|
|
1451
1652
|
}
|
|
1452
1653
|
if (error instanceof HttpError) {
|
|
1453
|
-
|
|
1654
|
+
writeStderr(
|
|
1454
1655
|
hostNotice + `harness: ${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}
|
|
1455
1656
|
`
|
|
1456
1657
|
);
|
|
1457
1658
|
return exitWith(1);
|
|
1458
1659
|
}
|
|
1459
1660
|
if (error instanceof CommandError) {
|
|
1460
|
-
|
|
1661
|
+
writeStderr(hostNotice + `harness: ${clean(error.message)}
|
|
1461
1662
|
`);
|
|
1462
1663
|
return exitWith(1);
|
|
1463
1664
|
}
|
|
@@ -1518,7 +1719,7 @@ program.command("show").description("full issue detail; downloads attached image
|
|
|
1518
1719
|
});
|
|
1519
1720
|
}
|
|
1520
1721
|
);
|
|
1521
|
-
program.command("activity").description("mixed issue activity: comments, creation, state and
|
|
1722
|
+
program.command("activity").description("mixed issue activity: comments, creation, state, assignee, wait-condition and field changes").argument("[id]").option(
|
|
1522
1723
|
"--limit <n>",
|
|
1523
1724
|
`entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
|
|
1524
1725
|
activityLimit
|
|
@@ -1759,15 +1960,213 @@ for (const [verb, satisfied] of [
|
|
|
1759
1960
|
["satisfy", true],
|
|
1760
1961
|
["unsatisfy", false]
|
|
1761
1962
|
]) {
|
|
1762
|
-
gate.command(verb).description(`${satisfied ? "tick" : "untick"} a gate on an issue, by its label or condition id`).argument("[issue]").argument("[gate]").option("--json", "emit JSON").action(async (issue, gateName, opts) => {
|
|
1963
|
+
gate.command(verb).description(`${satisfied ? "tick" : "untick"} a gate on an issue, by its key, label or condition id`).argument("[issue]").argument("[gate]").option("--json", "emit JSON").action(async (issue, gateName, opts) => {
|
|
1763
1964
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1764
1965
|
if (issue === void 0 || gateName === void 0) {
|
|
1765
|
-
throw new UsageError(`usage: harness gate ${verb} <issue> <gate-label-or-id>`);
|
|
1966
|
+
throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
|
|
1766
1967
|
}
|
|
1767
1968
|
return gateSet(ctx, issue, gateName, satisfied);
|
|
1768
1969
|
});
|
|
1769
1970
|
});
|
|
1770
1971
|
}
|
|
1972
|
+
program.command("conditions").description("an issue's wait conditions; with an outside expert's pxc_ credential, the gates they own on it").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
|
|
1973
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
1974
|
+
if (issue === void 0) throw new UsageError("usage: harness conditions <issue>");
|
|
1975
|
+
return conditionList(ctx, issue);
|
|
1976
|
+
});
|
|
1977
|
+
});
|
|
1978
|
+
program.command("assignments").description("an outside expert's steps and the gates they own on them (pxc_ credentials only)").option("--json", "emit JSON").action(async (opts) => {
|
|
1979
|
+
await withConfig(opts.json === true, false, (ctx) => assignments(ctx));
|
|
1980
|
+
});
|
|
1981
|
+
var NO_OBJECTIVE_TEXT = "By reference only: never the objective's text, which the API does not return because it can quote what Operator read; the workbench link opens it. Approval happens in the app.";
|
|
1982
|
+
program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
|
|
1983
|
+
"--status <states>",
|
|
1984
|
+
`comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
|
|
1985
|
+
).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`, integer).option("--all", `follow every page (up to ${JOBS_MAX_PAGES})`).option("--json", "emit JSON").addHelpText("after", `
|
|
1986
|
+
${NO_OBJECTIVE_TEXT}
|
|
1987
|
+
`).action(async (opts) => {
|
|
1988
|
+
requireJobsArgs(opts);
|
|
1989
|
+
await withConfig(opts.json === true, false, (ctx) => jobsList(ctx, opts));
|
|
1990
|
+
});
|
|
1991
|
+
program.command("job").description("one Operator job: status, plan, progress, window, spend, approval link, effects, artifacts, people").argument("[id]").option("--changes", "instead, its change set by reference (never values or targets), with the approval link").option("--people", "instead, who is on it by name: requester, execution principal, owner, approvers, collaborators").option("--json", "emit JSON").addHelpText("after", `
|
|
1992
|
+
${NO_OBJECTIVE_TEXT}
|
|
1993
|
+
`).action(async (id, opts) => {
|
|
1994
|
+
if (id === void 0) throw new UsageError("usage: harness job <job-id> [--changes] [--people]");
|
|
1995
|
+
requireJobId(id);
|
|
1996
|
+
await withConfig(
|
|
1997
|
+
opts.json === true,
|
|
1998
|
+
false,
|
|
1999
|
+
(ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
|
|
2000
|
+
);
|
|
2001
|
+
});
|
|
2002
|
+
program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)", integer).option("--through-waits", "keep following while a person has to act, instead of stopping").option("--json", "emit one JSON document when the watch ends").addHelpText(
|
|
2003
|
+
"after",
|
|
2004
|
+
`
|
|
2005
|
+
Polls the job's events every 2 s, and every 10 s while the job is paused or
|
|
2006
|
+
a person has to act. A person has to act exactly when the job's waiting_on is
|
|
2007
|
+
set: an approval, a person, reconciliation, verification or delivery. A job
|
|
2008
|
+
paused with nothing for a person to do resumes by itself, so watch follows it.
|
|
2009
|
+
It only reads: approval happens in the app, and the approval link is printed.
|
|
2010
|
+
A failed poll is retried 3 times (1, 2, 4 s, or the server's Retry-After).
|
|
2011
|
+
|
|
2012
|
+
Exit codes:
|
|
2013
|
+
0 the job completed (or stdout's reader went away, e.g. | head)
|
|
2014
|
+
${EXIT_WAITING_FOR_PERSON} a person has to act (not with --through-waits); the link is printed
|
|
2015
|
+
${EXIT_JOB_ENDED} the job failed or was cancelled
|
|
2016
|
+
${EXIT_INTERRUPTED} interrupted with Ctrl-C; stderr says where to resume (--after)
|
|
2017
|
+
1, 3 as for every command: an API error, or the API unreachable after the retries;
|
|
2018
|
+
stderr says where to resume, and --json still prints the events read so far
|
|
2019
|
+
2 a usage error
|
|
2020
|
+
`
|
|
2021
|
+
).action(async (id, opts) => {
|
|
2022
|
+
if (id === void 0) throw new UsageError("usage: harness watch <job-id>");
|
|
2023
|
+
requireJobId(id);
|
|
2024
|
+
const json2 = opts.json === true;
|
|
2025
|
+
await withConfig(
|
|
2026
|
+
json2,
|
|
2027
|
+
false,
|
|
2028
|
+
(ctx) => jobWatch(ctx, id, {
|
|
2029
|
+
after: opts.after,
|
|
2030
|
+
throughWaits: opts.throughWaits === true,
|
|
2031
|
+
collectEvents: json2,
|
|
2032
|
+
onLine: json2 ? void 0 : (line) => process.stdout.write(line),
|
|
2033
|
+
signal: watchStopSignal()
|
|
2034
|
+
})
|
|
2035
|
+
);
|
|
2036
|
+
});
|
|
2037
|
+
var NOT_RETRIED = `
|
|
2038
|
+
Not idempotent, so it is never retried, not even after a timeout or a server
|
|
2039
|
+
error. When a failure leaves the outcome unknown it says so and exits 1:
|
|
2040
|
+
check \`job <id>\` before trying again. Exit 3 means the request never
|
|
2041
|
+
reached the server, so nothing changed. It answers the job as \`job\` shows
|
|
2042
|
+
it. Approving stays in the app.
|
|
2043
|
+
`;
|
|
2044
|
+
program.command("steer").description("post an instruction to an Operator job, as Steer does in the app; earlier approvals stop applying").argument("[id]").argument("[instruction...]").option("--instruction-file <path>", 'read the instruction from a file ("-" for stdin)').option("--json", "emit JSON").addHelpText(
|
|
2045
|
+
"after",
|
|
2046
|
+
`
|
|
2047
|
+
Posts the instruction in the job's room, attributed to you, wherever the app
|
|
2048
|
+
offers Steer; the job's owner and collaborators may steer. Work it changes
|
|
2049
|
+
needs a fresh approval, and a running job stops its current run to take it.
|
|
2050
|
+
At most ${STEER_MAX_CHARACTERS} characters once trimmed.
|
|
2051
|
+
|
|
2052
|
+
Quote the instruction: unquoted words that look like options (--json, --help)
|
|
2053
|
+
are read as options, not text. Put -- before an instruction that starts with
|
|
2054
|
+
"-", or pass it with --instruction-file, which takes any text as it is.
|
|
2055
|
+
${NOT_RETRIED}`
|
|
2056
|
+
).action(async (id, words, opts) => {
|
|
2057
|
+
if (id === void 0 || words.length === 0 && opts.instructionFile === void 0) {
|
|
2058
|
+
throw new UsageError(
|
|
2059
|
+
"usage: harness steer <job-id> <instruction> | harness steer <job-id> --instruction-file <path>"
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
requireJobId(id);
|
|
2063
|
+
const instruction = instructionFrom(words, opts.instructionFile);
|
|
2064
|
+
await withConfig(opts.json === true, true, (ctx) => jobSteer(ctx, id, instruction));
|
|
2065
|
+
});
|
|
2066
|
+
program.command("cancel").description("cancel an Operator job; committed effects stay committed, and their counts are printed").argument("[id]").option("--json", "emit JSON").addHelpText(
|
|
2067
|
+
"after",
|
|
2068
|
+
`
|
|
2069
|
+
Ends the job, as Cancel does in the app; only its execution principal may.
|
|
2070
|
+
Staged effects that never ran count as unknown.
|
|
2071
|
+
${NOT_RETRIED}`
|
|
2072
|
+
).action(async (id, opts) => {
|
|
2073
|
+
if (id === void 0) throw new UsageError("usage: harness cancel <job-id>");
|
|
2074
|
+
requireJobId(id);
|
|
2075
|
+
await withConfig(opts.json === true, true, (ctx) => jobCancel(ctx, id));
|
|
2076
|
+
});
|
|
2077
|
+
program.command("pause").description("pause a running Operator job for a person, keeping its active-time budget").argument("[id]").option("--json", "emit JSON").addHelpText(
|
|
2078
|
+
"after",
|
|
2079
|
+
`
|
|
2080
|
+
Holds the job, as Pause does in the app; only its execution principal may.
|
|
2081
|
+
Continue it to run again.
|
|
2082
|
+
${NOT_RETRIED}`
|
|
2083
|
+
).action(async (id, opts) => {
|
|
2084
|
+
if (id === void 0) throw new UsageError("usage: harness pause <job-id>");
|
|
2085
|
+
requireJobId(id);
|
|
2086
|
+
await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
|
|
2087
|
+
});
|
|
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
|
+
});
|
|
2115
|
+
function once(flag) {
|
|
2116
|
+
return (value, previous) => {
|
|
2117
|
+
if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
|
|
2118
|
+
return value;
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
program.command("continue").description(
|
|
2122
|
+
"open a new audited window for an Operator job that ran out of time or waits on a person, as Continue does in the app"
|
|
2123
|
+
).argument("[id]").option("--json", "emit JSON").addHelpText(
|
|
2124
|
+
"after",
|
|
2125
|
+
`
|
|
2126
|
+
Only the job's execution principal, the person it runs as, may continue it. ${NOT_IDEMPOTENT} \`job <id>\` before continuing it again.
|
|
2127
|
+
`
|
|
2128
|
+
).action(async (id, opts) => {
|
|
2129
|
+
if (id === void 0) throw new UsageError("usage: harness continue <job-id>");
|
|
2130
|
+
requireJobId(id);
|
|
2131
|
+
await withConfig(opts.json === true, true, (ctx) => jobContinue(ctx, id));
|
|
2132
|
+
});
|
|
2133
|
+
var procedure = program.command("procedure").description("saved Operator procedures: procedure save|show|run");
|
|
2134
|
+
procedure.command("save").description("save a completed, verified job as a procedure, as Save procedure does in the app (its owner only)").argument("[job-id]").option("--name <name>", "the procedure's name (trimmed, at most 200 characters)", once("--name")).option("--json", "emit JSON").addHelpText(
|
|
2135
|
+
"after",
|
|
2136
|
+
`
|
|
2137
|
+
Saving again saves another procedure. ${NOT_IDEMPOTENT} the job's procedures (\`job <id>\`) before saving it again.
|
|
2138
|
+
`
|
|
2139
|
+
).action(async (jobId, opts) => {
|
|
2140
|
+
if (jobId === void 0) throw new UsageError('usage: harness procedure save <job-id> --name "\u2026"');
|
|
2141
|
+
requireJobId(jobId);
|
|
2142
|
+
const name = requireProcedureName(opts.name);
|
|
2143
|
+
await withConfig(opts.json === true, true, (ctx) => procedureSave(ctx, jobId, name));
|
|
2144
|
+
});
|
|
2145
|
+
procedure.command("show").description("one procedure: its source job, versions (digest, capabilities, actions) and the parameters a run takes").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
2146
|
+
if (id === void 0) throw new UsageError("usage: harness procedure show <procedure-id>");
|
|
2147
|
+
requireProcedureId(id);
|
|
2148
|
+
await withConfig(opts.json === true, false, (ctx) => procedureShow(ctx, id));
|
|
2149
|
+
});
|
|
2150
|
+
procedure.command("run").description("run a procedure as a new job, with fresh inputs, your current rights and an approval of its own in the app").argument("[id]").option("--version <n>", "the version expected to be current (default: whichever is current)", once("--version")).option("--param <name=value>", "a parameter, as a string; name:=<json> for a JSON value (repeatable)", collect, []).option("--params-file <path>", 'the parameters as one JSON object ("-" for stdin)', once("--params-file")).option("--group <id>", "the contact group to reconcile, when the procedure needs one", once("--group")).option("--source <id>", "the uploaded file to reconcile against, when the procedure needs one", once("--source")).option("--json", "emit JSON").addHelpText(
|
|
2151
|
+
"after",
|
|
2152
|
+
`
|
|
2153
|
+
Nothing of the saved job carries over: no grant, approval, delegation, collaborator or read material. Approving happens in the app. ${NOT_IDEMPOTENT} \`jobs\` for the new job before running it again.
|
|
2154
|
+
`
|
|
2155
|
+
).action(
|
|
2156
|
+
async (id, opts) => {
|
|
2157
|
+
if (id === void 0) {
|
|
2158
|
+
throw new UsageError("usage: harness procedure run <procedure-id> [--version N] [--param name=value]\u2026");
|
|
2159
|
+
}
|
|
2160
|
+
const request = prepareProcedureRun(id, {
|
|
2161
|
+
version: opts.version,
|
|
2162
|
+
param: opts.param,
|
|
2163
|
+
paramsFile: opts.paramsFile,
|
|
2164
|
+
group: opts.group,
|
|
2165
|
+
source: opts.source
|
|
2166
|
+
});
|
|
2167
|
+
await withConfig(opts.json === true, true, (ctx) => startProcedureRun(ctx, request));
|
|
2168
|
+
}
|
|
2169
|
+
);
|
|
1771
2170
|
var commentCommand = program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--body-file <path>", 'read the comment body from a file ("-" for stdin)').option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(
|
|
1772
2171
|
async (id, text, opts) => {
|
|
1773
2172
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -2047,9 +2446,40 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
|
|
|
2047
2446
|
return upload(ctx, path);
|
|
2048
2447
|
});
|
|
2049
2448
|
});
|
|
2449
|
+
var tools = program.command("tools").description(
|
|
2450
|
+
"the capabilities Operator can use now in this organization (switched-off ones never appear); write and send capabilities need approval in the app"
|
|
2451
|
+
).option("--domain <domain>", "only this domain (`tools domains` lists them)").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
2452
|
+
requireToolsOpts(opts);
|
|
2453
|
+
await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
|
|
2454
|
+
});
|
|
2455
|
+
tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (query, opts) => {
|
|
2456
|
+
const merged = { ...tools.opts(), ...opts };
|
|
2457
|
+
if (query.length === 0) {
|
|
2458
|
+
throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
|
|
2459
|
+
}
|
|
2460
|
+
requireSearchQuery(query.join(" "));
|
|
2461
|
+
requireToolsOpts(merged);
|
|
2462
|
+
await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query.join(" "), merged));
|
|
2463
|
+
});
|
|
2464
|
+
tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
|
|
2465
|
+
const parent = tools.opts();
|
|
2466
|
+
if (parent.domain !== void 0 || parent.cursor !== void 0 || parent.all === true) {
|
|
2467
|
+
throw new UsageError("usage: harness tools domains [--json] (it takes no --domain, --cursor or --all)");
|
|
2468
|
+
}
|
|
2469
|
+
await withConfig(opts.json === true || parent.json === true, false, (ctx) => toolsDomains(ctx));
|
|
2470
|
+
});
|
|
2471
|
+
program.command("tool").description("one capability Operator can use, with its whole input schema (or, for a read, its result schema)").argument("[id]").option("--schema <which>", "input (the default) or result, for a read").option("--json", "emit JSON").action(async (id, opts) => {
|
|
2472
|
+
if (id === void 0) throw new UsageError("usage: harness tool <capability-id> [--schema input|result]");
|
|
2473
|
+
requireCapabilityId(id);
|
|
2474
|
+
requireSchemaView(opts.schema);
|
|
2475
|
+
await withConfig(opts.json === true, false, (ctx) => toolShow(ctx, id, opts));
|
|
2476
|
+
});
|
|
2477
|
+
program.command("skills").description("the organization's skills assigned to Operator: identity and content checksum, never content").option("--json", "emit JSON").action(async (opts) => {
|
|
2478
|
+
await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
|
|
2479
|
+
});
|
|
2050
2480
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2051
|
-
const { startMcpServer } = await import("./server-
|
|
2052
|
-
await startMcpServer();
|
|
2481
|
+
const { startMcpServer } = await import("./server-KVRQBVXI.js");
|
|
2482
|
+
await startMcpServer(invokedAs);
|
|
2053
2483
|
});
|
|
2054
2484
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
2055
2485
|
keys.command("list").description("provider slugs the org has keys for and the env var each maps to \u2014 never the values").option("--json", "emit JSON").action(async (opts) => {
|
|
@@ -2078,11 +2508,11 @@ program.command("login").description("sign in via the browser (infra.one session
|
|
|
2078
2508
|
async (opts) => {
|
|
2079
2509
|
const repoLookup = findRepoConfig();
|
|
2080
2510
|
for (const warning of repoLookup.warnings)
|
|
2081
|
-
|
|
2511
|
+
writeStderr(warning + "\n");
|
|
2082
2512
|
const repoUrl = repoLookup.config?.url;
|
|
2083
2513
|
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
|
|
2084
2514
|
if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
|
|
2085
|
-
|
|
2515
|
+
writeStderr(
|
|
2086
2516
|
`harness: host ${url} comes from ${repoLookup.config?.path}
|
|
2087
2517
|
`
|
|
2088
2518
|
);
|
|
@@ -2098,18 +2528,27 @@ program.command("logout").description("revoke and forget the stored login").opti
|
|
|
2098
2528
|
program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
|
|
2099
2529
|
const resolved = resolveConfig();
|
|
2100
2530
|
for (const warning of resolved.warnings)
|
|
2101
|
-
|
|
2531
|
+
writeStderr(warning + "\n");
|
|
2102
2532
|
if (resolved.kind === "error") {
|
|
2103
|
-
|
|
2533
|
+
writeStderr(`harness: ${resolved.message}
|
|
2104
2534
|
`);
|
|
2105
2535
|
return exitWith(2);
|
|
2106
2536
|
}
|
|
2107
2537
|
if (resolved.kind === "needs-login") {
|
|
2108
|
-
|
|
2538
|
+
writeStderr(loginHelp(resolved.baseUrl));
|
|
2109
2539
|
return exitWith(2);
|
|
2110
2540
|
}
|
|
2111
2541
|
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
2112
|
-
|
|
2542
|
+
let result;
|
|
2543
|
+
try {
|
|
2544
|
+
result = await whoami(resolved.config, source);
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
if (!(error instanceof TransportError)) throw error;
|
|
2547
|
+
writeStderr(`harness: cannot reach the API: ${clean(error.message)}
|
|
2548
|
+
`);
|
|
2549
|
+
return exitWith(3);
|
|
2550
|
+
}
|
|
2551
|
+
await finishAuth(result, opts.json === true);
|
|
2113
2552
|
});
|
|
2114
2553
|
function storedLoginUrl() {
|
|
2115
2554
|
return readCredentials(defaultCredentialsPath())?.url;
|
|
@@ -2122,7 +2561,7 @@ async function finishAuth(result, json2) {
|
|
|
2122
2561
|
process.stdout.write(json2 ? json(result.json) : result.human);
|
|
2123
2562
|
return exitWith(0);
|
|
2124
2563
|
}
|
|
2125
|
-
|
|
2564
|
+
writeStderr(`harness: ${clean(result.message)}
|
|
2126
2565
|
`);
|
|
2127
2566
|
return exitWith(1);
|
|
2128
2567
|
}
|
|
@@ -2134,9 +2573,10 @@ if (argv[2] === UPDATE_CHECK_COMMAND) {
|
|
|
2134
2573
|
}
|
|
2135
2574
|
try {
|
|
2136
2575
|
const notice = updateCheck();
|
|
2137
|
-
if (notice !== null)
|
|
2576
|
+
if (notice !== null) writeStderr(notice);
|
|
2138
2577
|
} catch {
|
|
2139
2578
|
}
|
|
2579
|
+
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
2140
2580
|
if (argv.length <= 2) {
|
|
2141
2581
|
program.outputHelp({ error: true });
|
|
2142
2582
|
await exitWith(2);
|
|
@@ -2145,7 +2585,7 @@ try {
|
|
|
2145
2585
|
await program.parseAsync(argv);
|
|
2146
2586
|
} catch (error) {
|
|
2147
2587
|
if (error instanceof UsageError) {
|
|
2148
|
-
|
|
2588
|
+
writeStderr(error.message + "\n");
|
|
2149
2589
|
await exitWith(2);
|
|
2150
2590
|
}
|
|
2151
2591
|
if (error instanceof CommanderError) {
|