@allocator-one/harness-cli 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +540 -23
- package/dist/{chunk-F2YVTB2A.js → chunk-I2S2JVCQ.js} +2024 -127
- package/dist/cli.js +419 -51
- package/dist/{server-75QJASTY.js → server-O7CX2ZCC.js} +210 -15
- package/package.json +2 -1
- package/spec/openapi.json +12302 -3927
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ACTIVITY_MAX_LIMIT,
|
|
4
|
+
CLI_PACKAGE,
|
|
4
5
|
CLI_VERSION,
|
|
5
6
|
Client,
|
|
6
7
|
CommandError,
|
|
7
8
|
DEFAULT_BASE_URL,
|
|
9
|
+
EXIT_INTERRUPTED,
|
|
10
|
+
EXIT_JOB_ENDED,
|
|
11
|
+
EXIT_WAITING_FOR_PERSON,
|
|
12
|
+
EXPERT_ASSIGNMENTS_PATH,
|
|
13
|
+
EXPERT_LAPSED,
|
|
8
14
|
HttpError,
|
|
15
|
+
JOBS_MAX_LIMIT,
|
|
16
|
+
JOBS_MAX_PAGES,
|
|
17
|
+
JOB_STATES,
|
|
9
18
|
LocalError,
|
|
10
19
|
RELATION_CREATE_TYPES,
|
|
11
20
|
RELATION_UPDATE_TYPES,
|
|
21
|
+
STEER_MAX_CHARACTERS,
|
|
12
22
|
TransportError,
|
|
13
23
|
UpgradeRequiredError,
|
|
14
24
|
UsageError,
|
|
15
25
|
activity,
|
|
16
26
|
archive,
|
|
27
|
+
assignments,
|
|
17
28
|
attachImage,
|
|
18
29
|
blocked,
|
|
19
30
|
clean,
|
|
20
31
|
close,
|
|
21
32
|
comment,
|
|
22
33
|
comments,
|
|
34
|
+
conditionList,
|
|
23
35
|
create,
|
|
24
36
|
defaultCredentialsPath,
|
|
25
37
|
deleteComment,
|
|
@@ -31,9 +43,20 @@ import {
|
|
|
31
43
|
docUpdate,
|
|
32
44
|
editComment,
|
|
33
45
|
errorDetails,
|
|
46
|
+
expertSummary,
|
|
47
|
+
explainExpertRefusal,
|
|
34
48
|
findRepoConfig,
|
|
35
49
|
gateSet,
|
|
50
|
+
instructionFrom,
|
|
51
|
+
isExpertToken,
|
|
36
52
|
isHostTrusted,
|
|
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,10 @@ import {
|
|
|
55
78
|
planHold,
|
|
56
79
|
planRelease,
|
|
57
80
|
planShow,
|
|
81
|
+
prepareProcedureRun,
|
|
58
82
|
prime,
|
|
83
|
+
procedureSave,
|
|
84
|
+
procedureShow,
|
|
59
85
|
projectCreate,
|
|
60
86
|
projectList,
|
|
61
87
|
projectShow,
|
|
@@ -67,17 +93,27 @@ import {
|
|
|
67
93
|
relationList,
|
|
68
94
|
relationRm,
|
|
69
95
|
relationUpdate,
|
|
96
|
+
requireCapabilityId,
|
|
70
97
|
requireCloseArgs,
|
|
71
98
|
requireCommentId,
|
|
72
99
|
requireIdentifiers,
|
|
100
|
+
requireJobId,
|
|
101
|
+
requireJobsArgs,
|
|
73
102
|
requirePlanId,
|
|
103
|
+
requireProcedureId,
|
|
104
|
+
requireProcedureName,
|
|
74
105
|
requireReadyClaimArgs,
|
|
106
|
+
requireSchemaView,
|
|
107
|
+
requireSearchQuery,
|
|
108
|
+
requireToolsOpts,
|
|
75
109
|
requiredTeam,
|
|
76
110
|
resolveCacheHome,
|
|
77
111
|
resolveConfig,
|
|
78
112
|
resolveTextSources,
|
|
79
113
|
search,
|
|
80
114
|
show,
|
|
115
|
+
skillsList,
|
|
116
|
+
startProcedureRun,
|
|
81
117
|
stateAdd,
|
|
82
118
|
stateDelete,
|
|
83
119
|
stateOrder,
|
|
@@ -87,6 +123,10 @@ import {
|
|
|
87
123
|
teamList,
|
|
88
124
|
teamRename,
|
|
89
125
|
teamSetDefault,
|
|
126
|
+
toolShow,
|
|
127
|
+
toolsDomains,
|
|
128
|
+
toolsList,
|
|
129
|
+
toolsSearch,
|
|
90
130
|
trustHost,
|
|
91
131
|
update,
|
|
92
132
|
upload,
|
|
@@ -94,9 +134,10 @@ import {
|
|
|
94
134
|
validateBaseUrl,
|
|
95
135
|
validateIdempotencyKey,
|
|
96
136
|
version,
|
|
137
|
+
watchStopSignal,
|
|
97
138
|
workflowStates,
|
|
98
139
|
writeCredentials
|
|
99
|
-
} from "./chunk-
|
|
140
|
+
} from "./chunk-I2S2JVCQ.js";
|
|
100
141
|
|
|
101
142
|
// src/cli.ts
|
|
102
143
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -679,7 +720,9 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
679
720
|
async function manualLogin(baseUrl, options) {
|
|
680
721
|
const notify = options.notify ?? defaultNotify;
|
|
681
722
|
const prompt = options.prompt ?? defaultPrompt;
|
|
682
|
-
notify(
|
|
723
|
+
notify(
|
|
724
|
+
`Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below (an outside expert pastes the pxc_ credential minted in their expert view instead).`
|
|
725
|
+
);
|
|
683
726
|
const token = (await prompt()).trim();
|
|
684
727
|
if (token === "") return { ok: false, message: "no token entered" };
|
|
685
728
|
return verifyAndStore(baseUrl, token, options);
|
|
@@ -687,13 +730,15 @@ async function manualLogin(baseUrl, options) {
|
|
|
687
730
|
async function verifyAndStore(baseUrl, token, options) {
|
|
688
731
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
689
732
|
const client = bareClient(baseUrl, token, options);
|
|
733
|
+
const expert = isExpertToken(token);
|
|
690
734
|
try {
|
|
691
|
-
await client.get("/api/v1/teams");
|
|
735
|
+
await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/teams");
|
|
692
736
|
} catch (error) {
|
|
693
737
|
if (error instanceof HttpError) {
|
|
738
|
+
const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
|
|
694
739
|
return {
|
|
695
740
|
ok: false,
|
|
696
|
-
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored`
|
|
741
|
+
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${lapsed}`
|
|
697
742
|
};
|
|
698
743
|
}
|
|
699
744
|
if (error instanceof TransportError) {
|
|
@@ -709,33 +754,44 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
709
754
|
try {
|
|
710
755
|
writeCredentials(path, baseUrl, token);
|
|
711
756
|
} catch (error) {
|
|
712
|
-
await client
|
|
757
|
+
await revokeSession(client, token);
|
|
713
758
|
const detail = error instanceof Error ? error.message : String(error);
|
|
714
759
|
return {
|
|
715
760
|
ok: false,
|
|
716
|
-
message: `could not store credentials at ${path}: ${detail}`
|
|
761
|
+
message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : "")
|
|
717
762
|
};
|
|
718
763
|
}
|
|
764
|
+
let supersededNote = "";
|
|
719
765
|
if (superseded !== null) {
|
|
720
|
-
await bareClient(superseded.url, superseded.token, options).
|
|
766
|
+
await revokeSession(bareClient(superseded.url, superseded.token, options), superseded.token);
|
|
767
|
+
if (isExpertToken(superseded.token)) {
|
|
768
|
+
supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
|
|
769
|
+
}
|
|
721
770
|
}
|
|
722
771
|
return {
|
|
723
772
|
ok: true,
|
|
724
773
|
json: { url: baseUrl, credentials: path },
|
|
725
|
-
human: `
|
|
726
|
-
`
|
|
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 harness logout only forgets it). Commands now act as the expert: only assignments, conditions and gate work until you log in again.
|
|
775
|
+
` : `Logged in to ${baseUrl} \u2014 login token stored at ${path} (valid ~30 days; harness logout revokes it).
|
|
776
|
+
`) + supersededNote
|
|
727
777
|
};
|
|
728
778
|
}
|
|
779
|
+
async function revokeSession(client, token) {
|
|
780
|
+
if (isExpertToken(token)) return false;
|
|
781
|
+
return client.delete("/api/v1/session").then(() => true).catch(() => false);
|
|
782
|
+
}
|
|
729
783
|
async function logout(options = {}) {
|
|
730
784
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
731
785
|
const stored = readCredentials(path);
|
|
732
786
|
if (stored === null) {
|
|
787
|
+
const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
|
|
733
788
|
return {
|
|
734
789
|
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."
|
|
790
|
+
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
791
|
};
|
|
737
792
|
}
|
|
738
|
-
const
|
|
793
|
+
const expert = isExpertToken(stored.token);
|
|
794
|
+
const revoked = await revokeSession(bareClient(stored.url, stored.token, options), stored.token);
|
|
739
795
|
const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
|
|
740
796
|
try {
|
|
741
797
|
deleteCredentials(path);
|
|
@@ -749,23 +805,36 @@ async function logout(options = {}) {
|
|
|
749
805
|
return {
|
|
750
806
|
ok: true,
|
|
751
807
|
json: { logged_out: true, revoked },
|
|
752
|
-
human: `
|
|
808
|
+
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.
|
|
809
|
+
` : `Logged out of ${stored.url}${note}.
|
|
753
810
|
`
|
|
754
811
|
};
|
|
755
812
|
}
|
|
756
813
|
async function whoami(config, credentialSource) {
|
|
757
814
|
const client = new Client(config);
|
|
815
|
+
const expert = isExpertToken(config.token);
|
|
816
|
+
let probe;
|
|
758
817
|
try {
|
|
759
|
-
await client.get("/api/v1/stats");
|
|
818
|
+
probe = (await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats")).data;
|
|
760
819
|
} catch (error) {
|
|
761
820
|
if (error instanceof HttpError) {
|
|
762
821
|
return {
|
|
763
822
|
ok: false,
|
|
764
|
-
message: `signed in to ${config.baseUrl} with a ${credentialSource} credential, but it failed verification: ${error.message} (HTTP ${error.status})`
|
|
823
|
+
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
824
|
};
|
|
766
825
|
}
|
|
767
826
|
throw error;
|
|
768
827
|
}
|
|
828
|
+
let seat = null;
|
|
829
|
+
if (expert) {
|
|
830
|
+
seat = expertSeat(probe);
|
|
831
|
+
if (seat === null) {
|
|
832
|
+
return {
|
|
833
|
+
ok: false,
|
|
834
|
+
message: `signed in to ${config.baseUrl} with an outside expert's pxc_ credential, but the assignments response carried no expert`
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
769
838
|
const serverVersion = await client.serverVersion().catch(() => "unknown");
|
|
770
839
|
const hostSourceLabels = {
|
|
771
840
|
env: "HARNESS_API_URL",
|
|
@@ -774,6 +843,9 @@ async function whoami(config, credentialSource) {
|
|
|
774
843
|
default: "workspace default"
|
|
775
844
|
};
|
|
776
845
|
const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
|
|
846
|
+
const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
|
|
847
|
+
Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
|
|
848
|
+
`;
|
|
777
849
|
return {
|
|
778
850
|
ok: true,
|
|
779
851
|
json: {
|
|
@@ -781,14 +853,24 @@ async function whoami(config, credentialSource) {
|
|
|
781
853
|
source: credentialSource,
|
|
782
854
|
verified: true,
|
|
783
855
|
server_version: serverVersion,
|
|
784
|
-
host_source: config.hostSource
|
|
856
|
+
host_source: config.hostSource,
|
|
857
|
+
expert: seat
|
|
785
858
|
},
|
|
786
859
|
human: `Host: ${config.baseUrl}${via}
|
|
787
|
-
Credential: ${credentialSource}
|
|
788
|
-
Verified: ok (server API ${serverVersion})
|
|
860
|
+
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}
|
|
861
|
+
` + expertLines + `Verified: ok (server API ${serverVersion})
|
|
789
862
|
`
|
|
790
863
|
};
|
|
791
864
|
}
|
|
865
|
+
function expertSeat(data) {
|
|
866
|
+
if (!isRecord2(data) || !isRecord2(data["expert"])) return null;
|
|
867
|
+
const plan2 = isRecord2(data["plan"]) ? { id: data["plan"]["id"], status: data["plan"]["status"] } : null;
|
|
868
|
+
const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord2).length : 0;
|
|
869
|
+
return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
|
|
870
|
+
}
|
|
871
|
+
function isRecord2(value) {
|
|
872
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
873
|
+
}
|
|
792
874
|
function bareClient(baseUrl, token, options) {
|
|
793
875
|
const config = {
|
|
794
876
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
@@ -832,6 +914,57 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
|
832
914
|
}
|
|
833
915
|
}
|
|
834
916
|
|
|
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
|
+
|
|
835
968
|
// src/core/upgrade.ts
|
|
836
969
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
837
970
|
import {
|
|
@@ -847,13 +980,19 @@ import {
|
|
|
847
980
|
statSync,
|
|
848
981
|
writeFileSync
|
|
849
982
|
} from "fs";
|
|
850
|
-
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
983
|
+
import { basename as basename2, dirname, join as join2, parse, sep } from "path";
|
|
851
984
|
import { fileURLToPath } from "url";
|
|
852
985
|
var PACKAGE_NAME = "@allocator-one/harness-cli";
|
|
986
|
+
var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
|
|
853
987
|
var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
|
|
988
|
+
function upgradeInstallSpec(install) {
|
|
989
|
+
return `${install.packageName ?? PACKAGE_NAME}@latest`;
|
|
990
|
+
}
|
|
854
991
|
var UPDATE_CHECK_COMMAND = "__update-check";
|
|
855
992
|
var REEXEC_SENTINEL = "HARNESS_UPGRADED";
|
|
856
|
-
|
|
993
|
+
function latestUrl(packageName = PACKAGE_NAME) {
|
|
994
|
+
return `https://registry.npmjs.org/${packageName}/latest`;
|
|
995
|
+
}
|
|
857
996
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
858
997
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
859
998
|
var NPM_ROOT_TIMEOUT_MS = 15e3;
|
|
@@ -866,21 +1005,24 @@ function detectInstall(from = fileURLToPath(import.meta.url)) {
|
|
|
866
1005
|
nodeModules: null
|
|
867
1006
|
};
|
|
868
1007
|
if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
|
|
869
|
-
const
|
|
870
|
-
if (
|
|
1008
|
+
const found = findPackageRoot(from);
|
|
1009
|
+
if (found === null) return unknown;
|
|
1010
|
+
const { packageRoot, packageName } = found;
|
|
871
1011
|
if (existsSync(join2(packageRoot, ".git"))) {
|
|
872
|
-
return { kind: "checkout", packageRoot, nodeModules: null };
|
|
1012
|
+
return { kind: "checkout", packageRoot, packageName, nodeModules: null };
|
|
873
1013
|
}
|
|
874
1014
|
const nodeModules = enclosingNodeModules(packageRoot);
|
|
875
|
-
if (nodeModules === null) return { ...unknown, packageRoot };
|
|
1015
|
+
if (nodeModules === null) return { ...unknown, packageRoot, packageName };
|
|
876
1016
|
const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
|
|
877
|
-
return { kind, packageRoot, nodeModules };
|
|
1017
|
+
return { kind, packageRoot, packageName, nodeModules };
|
|
878
1018
|
}
|
|
879
1019
|
function findPackageRoot(from) {
|
|
880
1020
|
let dir = dirname(from);
|
|
881
1021
|
const { root } = parse(dir);
|
|
882
1022
|
for (; ; ) {
|
|
883
|
-
|
|
1023
|
+
const name = packageNameAt(join2(dir, "package.json"));
|
|
1024
|
+
if (name !== null && PACKAGE_NAMES.includes(name))
|
|
1025
|
+
return { packageRoot: dir, packageName: name };
|
|
884
1026
|
if (dir === root) return null;
|
|
885
1027
|
dir = dirname(dir);
|
|
886
1028
|
}
|
|
@@ -911,7 +1053,7 @@ function enclosingNodeModules(packageRoot) {
|
|
|
911
1053
|
let dir = dirname(packageRoot);
|
|
912
1054
|
const { root } = parse(dir);
|
|
913
1055
|
for (; ; ) {
|
|
914
|
-
if (
|
|
1056
|
+
if (basename2(dir) === "node_modules") return dir;
|
|
915
1057
|
if (dir === root) return null;
|
|
916
1058
|
dir = dirname(dir);
|
|
917
1059
|
}
|
|
@@ -958,12 +1100,15 @@ function comparePrerelease(left, right) {
|
|
|
958
1100
|
}
|
|
959
1101
|
return 0;
|
|
960
1102
|
}
|
|
961
|
-
function stampPath(env = process.env) {
|
|
962
|
-
|
|
1103
|
+
function stampPath(env = process.env, packageName = PACKAGE_NAME) {
|
|
1104
|
+
const file = packageName === PACKAGE_NAME ? "update-check.json" : `update-check-${packageName.replace(/^@[^/]+\//, "")}.json`;
|
|
1105
|
+
return join2(resolveCacheHome(env), "harness", file);
|
|
963
1106
|
}
|
|
964
|
-
function readStamp(env = process.env) {
|
|
1107
|
+
function readStamp(env = process.env, packageName = PACKAGE_NAME) {
|
|
965
1108
|
try {
|
|
966
|
-
const parsed = JSON.parse(
|
|
1109
|
+
const parsed = JSON.parse(
|
|
1110
|
+
readFileSync(stampPath(env, packageName), "utf8")
|
|
1111
|
+
);
|
|
967
1112
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
968
1113
|
const record = parsed;
|
|
969
1114
|
const checkedAt = record["checkedAt"];
|
|
@@ -975,10 +1120,10 @@ function readStamp(env = process.env) {
|
|
|
975
1120
|
return null;
|
|
976
1121
|
}
|
|
977
1122
|
}
|
|
978
|
-
function writeStamp(stamp, env = process.env) {
|
|
979
|
-
const path = stampPath(env);
|
|
1123
|
+
function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
|
|
1124
|
+
const path = stampPath(env, packageName);
|
|
980
1125
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
981
|
-
const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
|
|
1126
|
+
const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
|
|
982
1127
|
try {
|
|
983
1128
|
mkdirSync(dirname(path), { recursive: true });
|
|
984
1129
|
writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
|
|
@@ -996,9 +1141,10 @@ function updateCheck(options = {}) {
|
|
|
996
1141
|
if (!checksEnabled(env)) return null;
|
|
997
1142
|
const install = options.install ?? detectInstall();
|
|
998
1143
|
if (install.kind !== "global" && install.kind !== "local") return null;
|
|
999
|
-
const
|
|
1144
|
+
const packageName = install.packageName ?? PACKAGE_NAME;
|
|
1145
|
+
const stamp = readStamp(env, packageName);
|
|
1000
1146
|
if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
|
|
1001
|
-
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
|
|
1147
|
+
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
|
|
1002
1148
|
(options.refresh ?? spawnRefresh)();
|
|
1003
1149
|
}
|
|
1004
1150
|
if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
|
|
@@ -1008,7 +1154,7 @@ function updateCheck(options = {}) {
|
|
|
1008
1154
|
return null;
|
|
1009
1155
|
}
|
|
1010
1156
|
function upgradeHint(install) {
|
|
1011
|
-
return install.kind === "local" ? `npm i ${
|
|
1157
|
+
return install.kind === "local" ? `npm i ${upgradeInstallSpec(install)}` : `npm i -g ${upgradeInstallSpec(install)}`;
|
|
1012
1158
|
}
|
|
1013
1159
|
function spawnRefresh() {
|
|
1014
1160
|
const self = process.argv[1];
|
|
@@ -1024,13 +1170,17 @@ function spawnRefresh() {
|
|
|
1024
1170
|
} catch {
|
|
1025
1171
|
}
|
|
1026
1172
|
}
|
|
1027
|
-
async function performUpdateCheck(env = process.env, now = Date.now()) {
|
|
1173
|
+
async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
|
|
1028
1174
|
if (!checksEnabled(env)) return;
|
|
1029
|
-
writeStamp(
|
|
1175
|
+
writeStamp(
|
|
1176
|
+
{ checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
|
|
1177
|
+
env,
|
|
1178
|
+
packageName
|
|
1179
|
+
);
|
|
1030
1180
|
}
|
|
1031
|
-
async function fetchLatestVersion(env) {
|
|
1181
|
+
async function fetchLatestVersion(env, packageName) {
|
|
1032
1182
|
const configured = env["HARNESS_REGISTRY_URL"];
|
|
1033
|
-
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() :
|
|
1183
|
+
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
|
|
1034
1184
|
try {
|
|
1035
1185
|
const response = await fetch(url, {
|
|
1036
1186
|
headers: { accept: "application/json" },
|
|
@@ -1148,8 +1298,10 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1148
1298
|
detail: "lost the upgrade lock while preparing \u2014 another process took over"
|
|
1149
1299
|
};
|
|
1150
1300
|
}
|
|
1151
|
-
process.stderr.write(
|
|
1152
|
-
`
|
|
1301
|
+
process.stderr.write(
|
|
1302
|
+
`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1303
|
+
`
|
|
1304
|
+
);
|
|
1153
1305
|
const args = [
|
|
1154
1306
|
"install",
|
|
1155
1307
|
"--global",
|
|
@@ -1158,7 +1310,7 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1158
1310
|
"--no-fund",
|
|
1159
1311
|
"--no-update-notifier",
|
|
1160
1312
|
"--loglevel=error",
|
|
1161
|
-
|
|
1313
|
+
upgradeInstallSpec(install)
|
|
1162
1314
|
];
|
|
1163
1315
|
const result = spawnSync(npmCommand(), args, {
|
|
1164
1316
|
// npm's diagnostics belong on stderr with ours; stdout stays the
|
|
@@ -1320,7 +1472,10 @@ function exitWith(code) {
|
|
|
1320
1472
|
}
|
|
1321
1473
|
guardBrokenPipes();
|
|
1322
1474
|
var program = new Command();
|
|
1323
|
-
|
|
1475
|
+
var invokedAs = invokedName();
|
|
1476
|
+
program.name(invokedAs).description(
|
|
1477
|
+
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
|
+
).exitOverride().configureOutput({
|
|
1324
1479
|
writeOut: (text) => process.stdout.write(text),
|
|
1325
1480
|
writeErr: (text) => process.stderr.write(text)
|
|
1326
1481
|
});
|
|
@@ -1419,7 +1574,7 @@ async function execute(config, jsonFlag, mutating, run) {
|
|
|
1419
1574
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1420
1575
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1421
1576
|
} catch (error) {
|
|
1422
|
-
return fail(error, hostNotice);
|
|
1577
|
+
return fail(explainExpertRefusal(error, config.token), hostNotice);
|
|
1423
1578
|
}
|
|
1424
1579
|
}
|
|
1425
1580
|
async function fail(error, hostNotice) {
|
|
@@ -1518,7 +1673,7 @@ program.command("show").description("full issue detail; downloads attached image
|
|
|
1518
1673
|
});
|
|
1519
1674
|
}
|
|
1520
1675
|
);
|
|
1521
|
-
program.command("activity").description("mixed issue activity: comments, creation, state and
|
|
1676
|
+
program.command("activity").description("mixed issue activity: comments, creation, state, assignee, wait-condition and field changes").argument("[id]").option(
|
|
1522
1677
|
"--limit <n>",
|
|
1523
1678
|
`entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
|
|
1524
1679
|
activityLimit
|
|
@@ -1759,15 +1914,187 @@ for (const [verb, satisfied] of [
|
|
|
1759
1914
|
["satisfy", true],
|
|
1760
1915
|
["unsatisfy", false]
|
|
1761
1916
|
]) {
|
|
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) => {
|
|
1917
|
+
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
1918
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1764
1919
|
if (issue === void 0 || gateName === void 0) {
|
|
1765
|
-
throw new UsageError(`usage: harness gate ${verb} <issue> <gate-label-or-id>`);
|
|
1920
|
+
throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
|
|
1766
1921
|
}
|
|
1767
1922
|
return gateSet(ctx, issue, gateName, satisfied);
|
|
1768
1923
|
});
|
|
1769
1924
|
});
|
|
1770
1925
|
}
|
|
1926
|
+
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) => {
|
|
1927
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
1928
|
+
if (issue === void 0) throw new UsageError("usage: harness conditions <issue>");
|
|
1929
|
+
return conditionList(ctx, issue);
|
|
1930
|
+
});
|
|
1931
|
+
});
|
|
1932
|
+
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) => {
|
|
1933
|
+
await withConfig(opts.json === true, false, (ctx) => assignments(ctx));
|
|
1934
|
+
});
|
|
1935
|
+
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.";
|
|
1936
|
+
program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
|
|
1937
|
+
"--status <states>",
|
|
1938
|
+
`comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
|
|
1939
|
+
).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", `
|
|
1940
|
+
${NO_OBJECTIVE_TEXT}
|
|
1941
|
+
`).action(async (opts) => {
|
|
1942
|
+
requireJobsArgs(opts);
|
|
1943
|
+
await withConfig(opts.json === true, false, (ctx) => jobsList(ctx, opts));
|
|
1944
|
+
});
|
|
1945
|
+
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", `
|
|
1946
|
+
${NO_OBJECTIVE_TEXT}
|
|
1947
|
+
`).action(async (id, opts) => {
|
|
1948
|
+
if (id === void 0) throw new UsageError("usage: harness job <job-id> [--changes] [--people]");
|
|
1949
|
+
requireJobId(id);
|
|
1950
|
+
await withConfig(
|
|
1951
|
+
opts.json === true,
|
|
1952
|
+
false,
|
|
1953
|
+
(ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
|
|
1954
|
+
);
|
|
1955
|
+
});
|
|
1956
|
+
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(
|
|
1957
|
+
"after",
|
|
1958
|
+
`
|
|
1959
|
+
Polls the job's events every 2 s, and every 10 s while the job is paused or
|
|
1960
|
+
a person has to act. A person has to act exactly when the job's waiting_on is
|
|
1961
|
+
set: an approval, a person, reconciliation, verification or delivery. A job
|
|
1962
|
+
paused with nothing for a person to do resumes by itself, so watch follows it.
|
|
1963
|
+
It only reads: approval happens in the app, and the approval link is printed.
|
|
1964
|
+
A failed poll is retried 3 times (1, 2, 4 s, or the server's Retry-After).
|
|
1965
|
+
|
|
1966
|
+
Exit codes:
|
|
1967
|
+
0 the job completed (or stdout's reader went away, e.g. | head)
|
|
1968
|
+
${EXIT_WAITING_FOR_PERSON} a person has to act (not with --through-waits); the link is printed
|
|
1969
|
+
${EXIT_JOB_ENDED} the job failed or was cancelled
|
|
1970
|
+
${EXIT_INTERRUPTED} interrupted with Ctrl-C; stderr says where to resume (--after)
|
|
1971
|
+
1, 3 as for every command: an API error, or the API unreachable after the retries;
|
|
1972
|
+
stderr says where to resume, and --json still prints the events read so far
|
|
1973
|
+
2 a usage error
|
|
1974
|
+
`
|
|
1975
|
+
).action(async (id, opts) => {
|
|
1976
|
+
if (id === void 0) throw new UsageError("usage: harness watch <job-id>");
|
|
1977
|
+
requireJobId(id);
|
|
1978
|
+
const json2 = opts.json === true;
|
|
1979
|
+
await withConfig(
|
|
1980
|
+
json2,
|
|
1981
|
+
false,
|
|
1982
|
+
(ctx) => jobWatch(ctx, id, {
|
|
1983
|
+
after: opts.after,
|
|
1984
|
+
throughWaits: opts.throughWaits === true,
|
|
1985
|
+
collectEvents: json2,
|
|
1986
|
+
onLine: json2 ? void 0 : (line) => process.stdout.write(line),
|
|
1987
|
+
signal: watchStopSignal()
|
|
1988
|
+
})
|
|
1989
|
+
);
|
|
1990
|
+
});
|
|
1991
|
+
var NOT_RETRIED = `
|
|
1992
|
+
Not idempotent, so it is never retried, not even after a timeout or a server
|
|
1993
|
+
error. When a failure leaves the outcome unknown it says so and exits 1:
|
|
1994
|
+
check \`job <id>\` before trying again. Exit 3 means the request never
|
|
1995
|
+
reached the server, so nothing changed. It answers the job as \`job\` shows
|
|
1996
|
+
it. Approving stays in the app.
|
|
1997
|
+
`;
|
|
1998
|
+
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(
|
|
1999
|
+
"after",
|
|
2000
|
+
`
|
|
2001
|
+
Posts the instruction in the job's room, attributed to you, wherever the app
|
|
2002
|
+
offers Steer; the job's owner and collaborators may steer. Work it changes
|
|
2003
|
+
needs a fresh approval, and a running job stops its current run to take it.
|
|
2004
|
+
At most ${STEER_MAX_CHARACTERS} characters once trimmed.
|
|
2005
|
+
|
|
2006
|
+
Quote the instruction: unquoted words that look like options (--json, --help)
|
|
2007
|
+
are read as options, not text. Put -- before an instruction that starts with
|
|
2008
|
+
"-", or pass it with --instruction-file, which takes any text as it is.
|
|
2009
|
+
${NOT_RETRIED}`
|
|
2010
|
+
).action(async (id, words, opts) => {
|
|
2011
|
+
if (id === void 0 || words.length === 0 && opts.instructionFile === void 0) {
|
|
2012
|
+
throw new UsageError(
|
|
2013
|
+
"usage: harness steer <job-id> <instruction> | harness steer <job-id> --instruction-file <path>"
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
requireJobId(id);
|
|
2017
|
+
const instruction = instructionFrom(words, opts.instructionFile);
|
|
2018
|
+
await withConfig(opts.json === true, true, (ctx) => jobSteer(ctx, id, instruction));
|
|
2019
|
+
});
|
|
2020
|
+
program.command("cancel").description("cancel an Operator job; committed effects stay committed, and their counts are printed").argument("[id]").option("--json", "emit JSON").addHelpText(
|
|
2021
|
+
"after",
|
|
2022
|
+
`
|
|
2023
|
+
Ends the job, as Cancel does in the app; only its execution principal may.
|
|
2024
|
+
Staged effects that never ran count as unknown.
|
|
2025
|
+
${NOT_RETRIED}`
|
|
2026
|
+
).action(async (id, opts) => {
|
|
2027
|
+
if (id === void 0) throw new UsageError("usage: harness cancel <job-id>");
|
|
2028
|
+
requireJobId(id);
|
|
2029
|
+
await withConfig(opts.json === true, true, (ctx) => jobCancel(ctx, id));
|
|
2030
|
+
});
|
|
2031
|
+
program.command("pause").description("pause a running Operator job for a person, keeping its active-time budget").argument("[id]").option("--json", "emit JSON").addHelpText(
|
|
2032
|
+
"after",
|
|
2033
|
+
`
|
|
2034
|
+
Holds the job, as Pause does in the app; only its execution principal may.
|
|
2035
|
+
Continue it to run again.
|
|
2036
|
+
${NOT_RETRIED}`
|
|
2037
|
+
).action(async (id, opts) => {
|
|
2038
|
+
if (id === void 0) throw new UsageError("usage: harness pause <job-id>");
|
|
2039
|
+
requireJobId(id);
|
|
2040
|
+
await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
|
|
2041
|
+
});
|
|
2042
|
+
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";
|
|
2043
|
+
function once(flag) {
|
|
2044
|
+
return (value, previous) => {
|
|
2045
|
+
if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
|
|
2046
|
+
return value;
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
program.command("continue").description(
|
|
2050
|
+
"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"
|
|
2051
|
+
).argument("[id]").option("--json", "emit JSON").addHelpText(
|
|
2052
|
+
"after",
|
|
2053
|
+
`
|
|
2054
|
+
Only the job's execution principal, the person it runs as, may continue it. ${NOT_IDEMPOTENT} \`job <id>\` before continuing it again.
|
|
2055
|
+
`
|
|
2056
|
+
).action(async (id, opts) => {
|
|
2057
|
+
if (id === void 0) throw new UsageError("usage: harness continue <job-id>");
|
|
2058
|
+
requireJobId(id);
|
|
2059
|
+
await withConfig(opts.json === true, true, (ctx) => jobContinue(ctx, id));
|
|
2060
|
+
});
|
|
2061
|
+
var procedure = program.command("procedure").description("saved Operator procedures: procedure save|show|run");
|
|
2062
|
+
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(
|
|
2063
|
+
"after",
|
|
2064
|
+
`
|
|
2065
|
+
Saving again saves another procedure. ${NOT_IDEMPOTENT} the job's procedures (\`job <id>\`) before saving it again.
|
|
2066
|
+
`
|
|
2067
|
+
).action(async (jobId, opts) => {
|
|
2068
|
+
if (jobId === void 0) throw new UsageError('usage: harness procedure save <job-id> --name "\u2026"');
|
|
2069
|
+
requireJobId(jobId);
|
|
2070
|
+
const name = requireProcedureName(opts.name);
|
|
2071
|
+
await withConfig(opts.json === true, true, (ctx) => procedureSave(ctx, jobId, name));
|
|
2072
|
+
});
|
|
2073
|
+
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) => {
|
|
2074
|
+
if (id === void 0) throw new UsageError("usage: harness procedure show <procedure-id>");
|
|
2075
|
+
requireProcedureId(id);
|
|
2076
|
+
await withConfig(opts.json === true, false, (ctx) => procedureShow(ctx, id));
|
|
2077
|
+
});
|
|
2078
|
+
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(
|
|
2079
|
+
"after",
|
|
2080
|
+
`
|
|
2081
|
+
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.
|
|
2082
|
+
`
|
|
2083
|
+
).action(
|
|
2084
|
+
async (id, opts) => {
|
|
2085
|
+
if (id === void 0) {
|
|
2086
|
+
throw new UsageError("usage: harness procedure run <procedure-id> [--version N] [--param name=value]\u2026");
|
|
2087
|
+
}
|
|
2088
|
+
const request = prepareProcedureRun(id, {
|
|
2089
|
+
version: opts.version,
|
|
2090
|
+
param: opts.param,
|
|
2091
|
+
paramsFile: opts.paramsFile,
|
|
2092
|
+
group: opts.group,
|
|
2093
|
+
source: opts.source
|
|
2094
|
+
});
|
|
2095
|
+
await withConfig(opts.json === true, true, (ctx) => startProcedureRun(ctx, request));
|
|
2096
|
+
}
|
|
2097
|
+
);
|
|
1771
2098
|
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
2099
|
async (id, text, opts) => {
|
|
1773
2100
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -2047,9 +2374,40 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
|
|
|
2047
2374
|
return upload(ctx, path);
|
|
2048
2375
|
});
|
|
2049
2376
|
});
|
|
2377
|
+
var tools = program.command("tools").description(
|
|
2378
|
+
"the capabilities Operator can use now in this organization (switched-off ones never appear); write and send capabilities need approval in the app"
|
|
2379
|
+
).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) => {
|
|
2380
|
+
requireToolsOpts(opts);
|
|
2381
|
+
await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
|
|
2382
|
+
});
|
|
2383
|
+
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) => {
|
|
2384
|
+
const merged = { ...tools.opts(), ...opts };
|
|
2385
|
+
if (query.length === 0) {
|
|
2386
|
+
throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
|
|
2387
|
+
}
|
|
2388
|
+
requireSearchQuery(query.join(" "));
|
|
2389
|
+
requireToolsOpts(merged);
|
|
2390
|
+
await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query.join(" "), merged));
|
|
2391
|
+
});
|
|
2392
|
+
tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
|
|
2393
|
+
const parent = tools.opts();
|
|
2394
|
+
if (parent.domain !== void 0 || parent.cursor !== void 0 || parent.all === true) {
|
|
2395
|
+
throw new UsageError("usage: harness tools domains [--json] (it takes no --domain, --cursor or --all)");
|
|
2396
|
+
}
|
|
2397
|
+
await withConfig(opts.json === true || parent.json === true, false, (ctx) => toolsDomains(ctx));
|
|
2398
|
+
});
|
|
2399
|
+
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) => {
|
|
2400
|
+
if (id === void 0) throw new UsageError("usage: harness tool <capability-id> [--schema input|result]");
|
|
2401
|
+
requireCapabilityId(id);
|
|
2402
|
+
requireSchemaView(opts.schema);
|
|
2403
|
+
await withConfig(opts.json === true, false, (ctx) => toolShow(ctx, id, opts));
|
|
2404
|
+
});
|
|
2405
|
+
program.command("skills").description("the organization's skills assigned to Operator: identity and content checksum, never content").option("--json", "emit JSON").action(async (opts) => {
|
|
2406
|
+
await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
|
|
2407
|
+
});
|
|
2050
2408
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2051
|
-
const { startMcpServer } = await import("./server-
|
|
2052
|
-
await startMcpServer();
|
|
2409
|
+
const { startMcpServer } = await import("./server-O7CX2ZCC.js");
|
|
2410
|
+
await startMcpServer(invokedAs);
|
|
2053
2411
|
});
|
|
2054
2412
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
2055
2413
|
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) => {
|
|
@@ -2109,7 +2467,16 @@ program.command("whoami").description("show the resolved host and credential sou
|
|
|
2109
2467
|
return exitWith(2);
|
|
2110
2468
|
}
|
|
2111
2469
|
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
2112
|
-
|
|
2470
|
+
let result;
|
|
2471
|
+
try {
|
|
2472
|
+
result = await whoami(resolved.config, source);
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
if (!(error instanceof TransportError)) throw error;
|
|
2475
|
+
process.stderr.write(`harness: cannot reach the API: ${clean(error.message)}
|
|
2476
|
+
`);
|
|
2477
|
+
return exitWith(3);
|
|
2478
|
+
}
|
|
2479
|
+
await finishAuth(result, opts.json === true);
|
|
2113
2480
|
});
|
|
2114
2481
|
function storedLoginUrl() {
|
|
2115
2482
|
return readCredentials(defaultCredentialsPath())?.url;
|
|
@@ -2137,6 +2504,7 @@ try {
|
|
|
2137
2504
|
if (notice !== null) process.stderr.write(notice);
|
|
2138
2505
|
} catch {
|
|
2139
2506
|
}
|
|
2507
|
+
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
2140
2508
|
if (argv.length <= 2) {
|
|
2141
2509
|
program.outputHelp({ error: true });
|
|
2142
2510
|
await exitWith(2);
|