@allocator-one/harness-cli 1.13.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 +726 -31
- package/dist/chunk-I2S2JVCQ.js +4761 -0
- package/dist/cli.js +495 -173
- package/dist/{server-CYBKYG5E.js → server-O7CX2ZCC.js} +333 -15
- package/package.json +3 -1
- package/spec/openapi.json +13784 -1496
- package/dist/chunk-XFMECLOU.js +0 -2258
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,
|
|
@@ -30,8 +42,21 @@ import {
|
|
|
30
42
|
docShow,
|
|
31
43
|
docUpdate,
|
|
32
44
|
editComment,
|
|
45
|
+
errorDetails,
|
|
46
|
+
expertSummary,
|
|
47
|
+
explainExpertRefusal,
|
|
33
48
|
findRepoConfig,
|
|
49
|
+
gateSet,
|
|
50
|
+
instructionFrom,
|
|
51
|
+
isExpertToken,
|
|
34
52
|
isHostTrusted,
|
|
53
|
+
jobCancel,
|
|
54
|
+
jobContinue,
|
|
55
|
+
jobPause,
|
|
56
|
+
jobShow,
|
|
57
|
+
jobSteer,
|
|
58
|
+
jobWatch,
|
|
59
|
+
jobsList,
|
|
35
60
|
json,
|
|
36
61
|
labelCreate,
|
|
37
62
|
labelDelete,
|
|
@@ -48,24 +73,47 @@ import {
|
|
|
48
73
|
memberSetRole,
|
|
49
74
|
milestoneCreate,
|
|
50
75
|
milestoneList,
|
|
76
|
+
planCreate,
|
|
77
|
+
planEdit,
|
|
78
|
+
planHold,
|
|
79
|
+
planRelease,
|
|
80
|
+
planShow,
|
|
81
|
+
prepareProcedureRun,
|
|
51
82
|
prime,
|
|
83
|
+
procedureSave,
|
|
84
|
+
procedureShow,
|
|
52
85
|
projectCreate,
|
|
53
86
|
projectList,
|
|
54
87
|
projectShow,
|
|
55
88
|
projectUpdate,
|
|
56
89
|
readCredentials,
|
|
90
|
+
readPlanFile,
|
|
57
91
|
ready,
|
|
58
92
|
relationAdd,
|
|
59
93
|
relationList,
|
|
60
94
|
relationRm,
|
|
61
95
|
relationUpdate,
|
|
96
|
+
requireCapabilityId,
|
|
62
97
|
requireCloseArgs,
|
|
63
98
|
requireCommentId,
|
|
64
99
|
requireIdentifiers,
|
|
100
|
+
requireJobId,
|
|
101
|
+
requireJobsArgs,
|
|
102
|
+
requirePlanId,
|
|
103
|
+
requireProcedureId,
|
|
104
|
+
requireProcedureName,
|
|
105
|
+
requireReadyClaimArgs,
|
|
106
|
+
requireSchemaView,
|
|
107
|
+
requireSearchQuery,
|
|
108
|
+
requireToolsOpts,
|
|
109
|
+
requiredTeam,
|
|
65
110
|
resolveCacheHome,
|
|
66
111
|
resolveConfig,
|
|
112
|
+
resolveTextSources,
|
|
67
113
|
search,
|
|
68
114
|
show,
|
|
115
|
+
skillsList,
|
|
116
|
+
startProcedureRun,
|
|
69
117
|
stateAdd,
|
|
70
118
|
stateDelete,
|
|
71
119
|
stateOrder,
|
|
@@ -75,15 +123,21 @@ import {
|
|
|
75
123
|
teamList,
|
|
76
124
|
teamRename,
|
|
77
125
|
teamSetDefault,
|
|
126
|
+
toolShow,
|
|
127
|
+
toolsDomains,
|
|
128
|
+
toolsList,
|
|
129
|
+
toolsSearch,
|
|
78
130
|
trustHost,
|
|
79
131
|
update,
|
|
80
132
|
upload,
|
|
81
133
|
userList,
|
|
82
134
|
validateBaseUrl,
|
|
135
|
+
validateIdempotencyKey,
|
|
83
136
|
version,
|
|
137
|
+
watchStopSignal,
|
|
84
138
|
workflowStates,
|
|
85
139
|
writeCredentials
|
|
86
|
-
} from "./chunk-
|
|
140
|
+
} from "./chunk-I2S2JVCQ.js";
|
|
87
141
|
|
|
88
142
|
// src/cli.ts
|
|
89
143
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -666,7 +720,9 @@ async function exchangeCode(baseUrl, code, verifier, options) {
|
|
|
666
720
|
async function manualLogin(baseUrl, options) {
|
|
667
721
|
const notify = options.notify ?? defaultNotify;
|
|
668
722
|
const prompt = options.prompt ?? defaultPrompt;
|
|
669
|
-
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
|
+
);
|
|
670
726
|
const token = (await prompt()).trim();
|
|
671
727
|
if (token === "") return { ok: false, message: "no token entered" };
|
|
672
728
|
return verifyAndStore(baseUrl, token, options);
|
|
@@ -674,13 +730,15 @@ async function manualLogin(baseUrl, options) {
|
|
|
674
730
|
async function verifyAndStore(baseUrl, token, options) {
|
|
675
731
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
676
732
|
const client = bareClient(baseUrl, token, options);
|
|
733
|
+
const expert = isExpertToken(token);
|
|
677
734
|
try {
|
|
678
|
-
await client.get("/api/v1/teams");
|
|
735
|
+
await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/teams");
|
|
679
736
|
} catch (error) {
|
|
680
737
|
if (error instanceof HttpError) {
|
|
738
|
+
const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
|
|
681
739
|
return {
|
|
682
740
|
ok: false,
|
|
683
|
-
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}`
|
|
684
742
|
};
|
|
685
743
|
}
|
|
686
744
|
if (error instanceof TransportError) {
|
|
@@ -696,33 +754,44 @@ async function verifyAndStore(baseUrl, token, options) {
|
|
|
696
754
|
try {
|
|
697
755
|
writeCredentials(path, baseUrl, token);
|
|
698
756
|
} catch (error) {
|
|
699
|
-
await client
|
|
757
|
+
await revokeSession(client, token);
|
|
700
758
|
const detail = error instanceof Error ? error.message : String(error);
|
|
701
759
|
return {
|
|
702
760
|
ok: false,
|
|
703
|
-
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)" : "")
|
|
704
762
|
};
|
|
705
763
|
}
|
|
764
|
+
let supersededNote = "";
|
|
706
765
|
if (superseded !== null) {
|
|
707
|
-
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
|
+
}
|
|
708
770
|
}
|
|
709
771
|
return {
|
|
710
772
|
ok: true,
|
|
711
773
|
json: { url: baseUrl, credentials: path },
|
|
712
|
-
human: `
|
|
713
|
-
`
|
|
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
|
|
714
777
|
};
|
|
715
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
|
+
}
|
|
716
783
|
async function logout(options = {}) {
|
|
717
784
|
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
718
785
|
const stored = readCredentials(path);
|
|
719
786
|
if (stored === null) {
|
|
787
|
+
const envToken = (options.env ?? process.env)["HARNESS_API_TOKEN"] ?? "";
|
|
720
788
|
return {
|
|
721
789
|
ok: false,
|
|
722
|
-
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.")
|
|
723
791
|
};
|
|
724
792
|
}
|
|
725
|
-
const
|
|
793
|
+
const expert = isExpertToken(stored.token);
|
|
794
|
+
const revoked = await revokeSession(bareClient(stored.url, stored.token, options), stored.token);
|
|
726
795
|
const note = revoked ? "" : " (server-side revoke failed \u2014 the token still expires on its own)";
|
|
727
796
|
try {
|
|
728
797
|
deleteCredentials(path);
|
|
@@ -736,23 +805,36 @@ async function logout(options = {}) {
|
|
|
736
805
|
return {
|
|
737
806
|
ok: true,
|
|
738
807
|
json: { logged_out: true, revoked },
|
|
739
|
-
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}.
|
|
740
810
|
`
|
|
741
811
|
};
|
|
742
812
|
}
|
|
743
813
|
async function whoami(config, credentialSource) {
|
|
744
814
|
const client = new Client(config);
|
|
815
|
+
const expert = isExpertToken(config.token);
|
|
816
|
+
let probe;
|
|
745
817
|
try {
|
|
746
|
-
await client.get("/api/v1/stats");
|
|
818
|
+
probe = (await client.get(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats")).data;
|
|
747
819
|
} catch (error) {
|
|
748
820
|
if (error instanceof HttpError) {
|
|
749
821
|
return {
|
|
750
822
|
ok: false,
|
|
751
|
-
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})`
|
|
752
824
|
};
|
|
753
825
|
}
|
|
754
826
|
throw error;
|
|
755
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
|
+
}
|
|
756
838
|
const serverVersion = await client.serverVersion().catch(() => "unknown");
|
|
757
839
|
const hostSourceLabels = {
|
|
758
840
|
env: "HARNESS_API_URL",
|
|
@@ -761,6 +843,9 @@ async function whoami(config, credentialSource) {
|
|
|
761
843
|
default: "workspace default"
|
|
762
844
|
};
|
|
763
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
|
+
`;
|
|
764
849
|
return {
|
|
765
850
|
ok: true,
|
|
766
851
|
json: {
|
|
@@ -768,14 +853,24 @@ async function whoami(config, credentialSource) {
|
|
|
768
853
|
source: credentialSource,
|
|
769
854
|
verified: true,
|
|
770
855
|
server_version: serverVersion,
|
|
771
|
-
host_source: config.hostSource
|
|
856
|
+
host_source: config.hostSource,
|
|
857
|
+
expert: seat
|
|
772
858
|
},
|
|
773
859
|
human: `Host: ${config.baseUrl}${via}
|
|
774
|
-
Credential: ${credentialSource}
|
|
775
|
-
Verified: ok (server API ${serverVersion})
|
|
860
|
+
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}
|
|
861
|
+
` + expertLines + `Verified: ok (server API ${serverVersion})
|
|
776
862
|
`
|
|
777
863
|
};
|
|
778
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
|
+
}
|
|
779
874
|
function bareClient(baseUrl, token, options) {
|
|
780
875
|
const config = {
|
|
781
876
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
@@ -809,113 +904,6 @@ function defaultOpenBrowser(url) {
|
|
|
809
904
|
}
|
|
810
905
|
}
|
|
811
906
|
|
|
812
|
-
// src/core/textinput.ts
|
|
813
|
-
import { readFileSync, statSync } from "fs";
|
|
814
|
-
import { isatty } from "tty";
|
|
815
|
-
var STDIN_PATH = "-";
|
|
816
|
-
var MAX_TEXT_BYTES = 1e6;
|
|
817
|
-
function flagOf(name) {
|
|
818
|
-
return name.replace(/[A-Z]/g, (upper) => `-${upper.toLowerCase()}`);
|
|
819
|
-
}
|
|
820
|
-
function resolveTextSources(sources) {
|
|
821
|
-
const resolved = {};
|
|
822
|
-
const toRead = [];
|
|
823
|
-
let stdinClaimedBy;
|
|
824
|
-
for (const { name, inline, file } of sources) {
|
|
825
|
-
if (inline !== void 0 && file !== void 0) {
|
|
826
|
-
throw new UsageError(
|
|
827
|
-
`--${flagOf(name)} and --${flagOf(name)}-file both given \u2014 pass one; there is no precedence between them`
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
if (file === void 0) {
|
|
831
|
-
if (inline !== void 0) resolved[name] = inline;
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (file === STDIN_PATH) {
|
|
835
|
-
if (isatty(0)) {
|
|
836
|
-
throw new UsageError(
|
|
837
|
-
`--${flagOf(name)}-file - reads stdin, but stdin is a terminal \u2014 pipe something in or pass a path`
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
if (stdinClaimedBy !== void 0) {
|
|
841
|
-
throw new UsageError(
|
|
842
|
-
`--${flagOf(name)}-file and --${flagOf(stdinClaimedBy)}-file both read stdin \u2014 there is only one stream, so only one may be "-"`
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
stdinClaimedBy = name;
|
|
846
|
-
}
|
|
847
|
-
if (file !== STDIN_PATH) checkReadable(file, name);
|
|
848
|
-
toRead.push({ name, file });
|
|
849
|
-
}
|
|
850
|
-
for (const { name, file } of [...toRead].sort(byStdinLast)) {
|
|
851
|
-
resolved[name] = stripOneTrailingNewline(readTextFile(file, name));
|
|
852
|
-
}
|
|
853
|
-
return resolved;
|
|
854
|
-
}
|
|
855
|
-
function readTextFile(path, name) {
|
|
856
|
-
if (path === STDIN_PATH) {
|
|
857
|
-
let piped;
|
|
858
|
-
try {
|
|
859
|
-
piped = readFileSync(0);
|
|
860
|
-
} catch (error) {
|
|
861
|
-
throw new UsageError(`--${flagOf(name)}-file -: cannot read stdin (${describe(error)})`);
|
|
862
|
-
}
|
|
863
|
-
if (piped.length > MAX_TEXT_BYTES) {
|
|
864
|
-
throw new UsageError(oversize(`--${flagOf(name)}-file -`));
|
|
865
|
-
}
|
|
866
|
-
return decodeUtf8(piped, `--${flagOf(name)}-file -`);
|
|
867
|
-
}
|
|
868
|
-
const flag = `--${flagOf(name)}-file ${path}`;
|
|
869
|
-
let bytes;
|
|
870
|
-
try {
|
|
871
|
-
bytes = readFileSync(path);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
874
|
-
}
|
|
875
|
-
if (bytes.length > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
|
|
876
|
-
return decodeUtf8(bytes, flag);
|
|
877
|
-
}
|
|
878
|
-
function decodeUtf8(bytes, flag) {
|
|
879
|
-
try {
|
|
880
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
881
|
-
} catch {
|
|
882
|
-
throw new UsageError(`${flag}: not valid UTF-8 \u2014 convert it first`);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
function oversize(what) {
|
|
886
|
-
return `${what}: over ${MAX_TEXT_BYTES / 1e6} MB \u2014 that is not prose; check the path`;
|
|
887
|
-
}
|
|
888
|
-
function checkReadable(path, name) {
|
|
889
|
-
const flag = `--${flagOf(name)}-file ${path}`;
|
|
890
|
-
let stats2;
|
|
891
|
-
try {
|
|
892
|
-
stats2 = statSync(path);
|
|
893
|
-
} catch (error) {
|
|
894
|
-
throw new UsageError(`${flag}: ${describe(error)}`);
|
|
895
|
-
}
|
|
896
|
-
if (!stats2.isFile()) {
|
|
897
|
-
throw new UsageError(
|
|
898
|
-
`${flag}: ${stats2.isDirectory() ? "is a directory" : "not a regular file"}`
|
|
899
|
-
);
|
|
900
|
-
}
|
|
901
|
-
if (stats2.size > MAX_TEXT_BYTES) throw new UsageError(oversize(flag));
|
|
902
|
-
}
|
|
903
|
-
function byStdinLast(a, b) {
|
|
904
|
-
return Number(a.file === STDIN_PATH) - Number(b.file === STDIN_PATH);
|
|
905
|
-
}
|
|
906
|
-
function stripOneTrailingNewline(text) {
|
|
907
|
-
if (text.endsWith("\r\n")) return text.slice(0, -2);
|
|
908
|
-
if (text.endsWith("\n")) return text.slice(0, -1);
|
|
909
|
-
return text;
|
|
910
|
-
}
|
|
911
|
-
function describe(error) {
|
|
912
|
-
const code = error?.code;
|
|
913
|
-
if (code === "ENOENT") return "no such file";
|
|
914
|
-
if (code === "EISDIR") return "is a directory";
|
|
915
|
-
if (code === "EACCES") return "permission denied";
|
|
916
|
-
return error instanceof Error ? error.message : String(error);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
907
|
// src/core/stdio.ts
|
|
920
908
|
var BROKEN_PIPE_CODES = /* @__PURE__ */ new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
|
|
921
909
|
function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
@@ -926,6 +914,57 @@ function guardBrokenPipes(streams = [process.stdout, process.stderr]) {
|
|
|
926
914
|
}
|
|
927
915
|
}
|
|
928
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
|
+
|
|
929
968
|
// src/core/upgrade.ts
|
|
930
969
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
931
970
|
import {
|
|
@@ -934,20 +973,26 @@ import {
|
|
|
934
973
|
existsSync,
|
|
935
974
|
linkSync,
|
|
936
975
|
mkdirSync,
|
|
937
|
-
readFileSync
|
|
976
|
+
readFileSync,
|
|
938
977
|
realpathSync,
|
|
939
978
|
renameSync,
|
|
940
979
|
rmSync,
|
|
941
|
-
statSync
|
|
980
|
+
statSync,
|
|
942
981
|
writeFileSync
|
|
943
982
|
} from "fs";
|
|
944
|
-
import { basename, dirname, join as join2, parse, sep } from "path";
|
|
983
|
+
import { basename as basename2, dirname, join as join2, parse, sep } from "path";
|
|
945
984
|
import { fileURLToPath } from "url";
|
|
946
985
|
var PACKAGE_NAME = "@allocator-one/harness-cli";
|
|
986
|
+
var PACKAGE_NAMES = [PACKAGE_NAME, OPERATOR_PACKAGE];
|
|
947
987
|
var UPGRADE_INSTALL_SPEC = `${PACKAGE_NAME}@latest`;
|
|
988
|
+
function upgradeInstallSpec(install) {
|
|
989
|
+
return `${install.packageName ?? PACKAGE_NAME}@latest`;
|
|
990
|
+
}
|
|
948
991
|
var UPDATE_CHECK_COMMAND = "__update-check";
|
|
949
992
|
var REEXEC_SENTINEL = "HARNESS_UPGRADED";
|
|
950
|
-
|
|
993
|
+
function latestUrl(packageName = PACKAGE_NAME) {
|
|
994
|
+
return `https://registry.npmjs.org/${packageName}/latest`;
|
|
995
|
+
}
|
|
951
996
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
952
997
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
953
998
|
var NPM_ROOT_TIMEOUT_MS = 15e3;
|
|
@@ -960,28 +1005,31 @@ function detectInstall(from = fileURLToPath(import.meta.url)) {
|
|
|
960
1005
|
nodeModules: null
|
|
961
1006
|
};
|
|
962
1007
|
if (from.split(sep).includes("_npx")) return { ...unknown, kind: "npx" };
|
|
963
|
-
const
|
|
964
|
-
if (
|
|
1008
|
+
const found = findPackageRoot(from);
|
|
1009
|
+
if (found === null) return unknown;
|
|
1010
|
+
const { packageRoot, packageName } = found;
|
|
965
1011
|
if (existsSync(join2(packageRoot, ".git"))) {
|
|
966
|
-
return { kind: "checkout", packageRoot, nodeModules: null };
|
|
1012
|
+
return { kind: "checkout", packageRoot, packageName, nodeModules: null };
|
|
967
1013
|
}
|
|
968
1014
|
const nodeModules = enclosingNodeModules(packageRoot);
|
|
969
|
-
if (nodeModules === null) return { ...unknown, packageRoot };
|
|
1015
|
+
if (nodeModules === null) return { ...unknown, packageRoot, packageName };
|
|
970
1016
|
const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
|
|
971
|
-
return { kind, packageRoot, nodeModules };
|
|
1017
|
+
return { kind, packageRoot, packageName, nodeModules };
|
|
972
1018
|
}
|
|
973
1019
|
function findPackageRoot(from) {
|
|
974
1020
|
let dir = dirname(from);
|
|
975
1021
|
const { root } = parse(dir);
|
|
976
1022
|
for (; ; ) {
|
|
977
|
-
|
|
1023
|
+
const name = packageNameAt(join2(dir, "package.json"));
|
|
1024
|
+
if (name !== null && PACKAGE_NAMES.includes(name))
|
|
1025
|
+
return { packageRoot: dir, packageName: name };
|
|
978
1026
|
if (dir === root) return null;
|
|
979
1027
|
dir = dirname(dir);
|
|
980
1028
|
}
|
|
981
1029
|
}
|
|
982
1030
|
function packageNameAt(path) {
|
|
983
1031
|
try {
|
|
984
|
-
const parsed = JSON.parse(
|
|
1032
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
985
1033
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
986
1034
|
const name = parsed["name"];
|
|
987
1035
|
return typeof name === "string" ? name : null;
|
|
@@ -992,7 +1040,7 @@ function packageNameAt(path) {
|
|
|
992
1040
|
function versionAt(packageRoot) {
|
|
993
1041
|
try {
|
|
994
1042
|
const parsed = JSON.parse(
|
|
995
|
-
|
|
1043
|
+
readFileSync(join2(packageRoot, "package.json"), "utf8")
|
|
996
1044
|
);
|
|
997
1045
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
998
1046
|
const version2 = parsed["version"];
|
|
@@ -1005,7 +1053,7 @@ function enclosingNodeModules(packageRoot) {
|
|
|
1005
1053
|
let dir = dirname(packageRoot);
|
|
1006
1054
|
const { root } = parse(dir);
|
|
1007
1055
|
for (; ; ) {
|
|
1008
|
-
if (
|
|
1056
|
+
if (basename2(dir) === "node_modules") return dir;
|
|
1009
1057
|
if (dir === root) return null;
|
|
1010
1058
|
dir = dirname(dir);
|
|
1011
1059
|
}
|
|
@@ -1052,12 +1100,15 @@ function comparePrerelease(left, right) {
|
|
|
1052
1100
|
}
|
|
1053
1101
|
return 0;
|
|
1054
1102
|
}
|
|
1055
|
-
function stampPath(env = process.env) {
|
|
1056
|
-
|
|
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);
|
|
1057
1106
|
}
|
|
1058
|
-
function readStamp(env = process.env) {
|
|
1107
|
+
function readStamp(env = process.env, packageName = PACKAGE_NAME) {
|
|
1059
1108
|
try {
|
|
1060
|
-
const parsed = JSON.parse(
|
|
1109
|
+
const parsed = JSON.parse(
|
|
1110
|
+
readFileSync(stampPath(env, packageName), "utf8")
|
|
1111
|
+
);
|
|
1061
1112
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1062
1113
|
const record = parsed;
|
|
1063
1114
|
const checkedAt = record["checkedAt"];
|
|
@@ -1069,10 +1120,10 @@ function readStamp(env = process.env) {
|
|
|
1069
1120
|
return null;
|
|
1070
1121
|
}
|
|
1071
1122
|
}
|
|
1072
|
-
function writeStamp(stamp, env = process.env) {
|
|
1073
|
-
const path = stampPath(env);
|
|
1123
|
+
function writeStamp(stamp, env = process.env, packageName = PACKAGE_NAME) {
|
|
1124
|
+
const path = stampPath(env, packageName);
|
|
1074
1125
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
1075
|
-
const latest = stamp.latest ?? readStamp(env)?.latest ?? null;
|
|
1126
|
+
const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
|
|
1076
1127
|
try {
|
|
1077
1128
|
mkdirSync(dirname(path), { recursive: true });
|
|
1078
1129
|
writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
|
|
@@ -1090,9 +1141,10 @@ function updateCheck(options = {}) {
|
|
|
1090
1141
|
if (!checksEnabled(env)) return null;
|
|
1091
1142
|
const install = options.install ?? detectInstall();
|
|
1092
1143
|
if (install.kind !== "global" && install.kind !== "local") return null;
|
|
1093
|
-
const
|
|
1144
|
+
const packageName = install.packageName ?? PACKAGE_NAME;
|
|
1145
|
+
const stamp = readStamp(env, packageName);
|
|
1094
1146
|
if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
|
|
1095
|
-
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env);
|
|
1147
|
+
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
|
|
1096
1148
|
(options.refresh ?? spawnRefresh)();
|
|
1097
1149
|
}
|
|
1098
1150
|
if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
|
|
@@ -1102,7 +1154,7 @@ function updateCheck(options = {}) {
|
|
|
1102
1154
|
return null;
|
|
1103
1155
|
}
|
|
1104
1156
|
function upgradeHint(install) {
|
|
1105
|
-
return install.kind === "local" ? `npm i ${
|
|
1157
|
+
return install.kind === "local" ? `npm i ${upgradeInstallSpec(install)}` : `npm i -g ${upgradeInstallSpec(install)}`;
|
|
1106
1158
|
}
|
|
1107
1159
|
function spawnRefresh() {
|
|
1108
1160
|
const self = process.argv[1];
|
|
@@ -1118,13 +1170,17 @@ function spawnRefresh() {
|
|
|
1118
1170
|
} catch {
|
|
1119
1171
|
}
|
|
1120
1172
|
}
|
|
1121
|
-
async function performUpdateCheck(env = process.env, now = Date.now()) {
|
|
1173
|
+
async function performUpdateCheck(env = process.env, now = Date.now(), packageName = detectInstall().packageName ?? PACKAGE_NAME) {
|
|
1122
1174
|
if (!checksEnabled(env)) return;
|
|
1123
|
-
writeStamp(
|
|
1175
|
+
writeStamp(
|
|
1176
|
+
{ checkedAt: now, latest: await fetchLatestVersion(env, packageName) },
|
|
1177
|
+
env,
|
|
1178
|
+
packageName
|
|
1179
|
+
);
|
|
1124
1180
|
}
|
|
1125
|
-
async function fetchLatestVersion(env) {
|
|
1181
|
+
async function fetchLatestVersion(env, packageName) {
|
|
1126
1182
|
const configured = env["HARNESS_REGISTRY_URL"];
|
|
1127
|
-
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() :
|
|
1183
|
+
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
|
|
1128
1184
|
try {
|
|
1129
1185
|
const response = await fetch(url, {
|
|
1130
1186
|
headers: { accept: "application/json" },
|
|
@@ -1242,8 +1298,10 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1242
1298
|
detail: "lost the upgrade lock while preparing \u2014 another process took over"
|
|
1243
1299
|
};
|
|
1244
1300
|
}
|
|
1245
|
-
process.stderr.write(
|
|
1246
|
-
`
|
|
1301
|
+
process.stderr.write(
|
|
1302
|
+
`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1303
|
+
`
|
|
1304
|
+
);
|
|
1247
1305
|
const args = [
|
|
1248
1306
|
"install",
|
|
1249
1307
|
"--global",
|
|
@@ -1252,7 +1310,7 @@ function npmInstallGlobal(install, stillHeld) {
|
|
|
1252
1310
|
"--no-fund",
|
|
1253
1311
|
"--no-update-notifier",
|
|
1254
1312
|
"--loglevel=error",
|
|
1255
|
-
|
|
1313
|
+
upgradeInstallSpec(install)
|
|
1256
1314
|
];
|
|
1257
1315
|
const result = spawnSync(npmCommand(), args, {
|
|
1258
1316
|
// npm's diagnostics belong on stderr with ours; stdout stays the
|
|
@@ -1362,8 +1420,8 @@ function releaseLock(lock) {
|
|
|
1362
1420
|
}
|
|
1363
1421
|
function readHolder(path) {
|
|
1364
1422
|
try {
|
|
1365
|
-
const mtimeMs =
|
|
1366
|
-
const pid = Number.parseInt(
|
|
1423
|
+
const mtimeMs = statSync(path).mtimeMs;
|
|
1424
|
+
const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
1367
1425
|
return { pid: Number.isInteger(pid) && pid > 0 ? pid : null, mtimeMs };
|
|
1368
1426
|
} catch {
|
|
1369
1427
|
return null;
|
|
@@ -1414,7 +1472,10 @@ function exitWith(code) {
|
|
|
1414
1472
|
}
|
|
1415
1473
|
guardBrokenPipes();
|
|
1416
1474
|
var program = new Command();
|
|
1417
|
-
|
|
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({
|
|
1418
1479
|
writeOut: (text) => process.stdout.write(text),
|
|
1419
1480
|
writeErr: (text) => process.stderr.write(text)
|
|
1420
1481
|
});
|
|
@@ -1513,7 +1574,7 @@ async function execute(config, jsonFlag, mutating, run) {
|
|
|
1513
1574
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1514
1575
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1515
1576
|
} catch (error) {
|
|
1516
|
-
return fail(error, hostNotice);
|
|
1577
|
+
return fail(explainExpertRefusal(error, config.token), hostNotice);
|
|
1517
1578
|
}
|
|
1518
1579
|
}
|
|
1519
1580
|
async function fail(error, hostNotice) {
|
|
@@ -1557,17 +1618,11 @@ async function fail(error, hostNotice) {
|
|
|
1557
1618
|
}
|
|
1558
1619
|
throw error;
|
|
1559
1620
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const text = Array.isArray(messages) ? messages.map(String).join("; ") : String(messages);
|
|
1566
|
-
return `
|
|
1567
|
-
${clean(field)}: ${clean(text)}`;
|
|
1568
|
-
}).join("");
|
|
1569
|
-
}
|
|
1570
|
-
program.command("ready").description("issues ready to work, highest priority first").option("--claim", "atomically claim the first claimable issue").option("--team <key>", "team filter").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1621
|
+
program.command("ready").description("issues ready to work, highest priority first").option(
|
|
1622
|
+
"--claim",
|
|
1623
|
+
"claim the team's next ready, unassigned issue in one server-side request (team from --team, HARNESS_TEAM or the repo config; no paging flags)"
|
|
1624
|
+
).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer).option("--page <n>", "page number", integer).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1625
|
+
if (opts.claim === true) requireReadyClaimArgs(opts);
|
|
1571
1626
|
await withConfig(
|
|
1572
1627
|
opts.json === true,
|
|
1573
1628
|
opts.claim === true,
|
|
@@ -1618,7 +1673,7 @@ program.command("show").description("full issue detail; downloads attached image
|
|
|
1618
1673
|
});
|
|
1619
1674
|
}
|
|
1620
1675
|
);
|
|
1621
|
-
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(
|
|
1622
1677
|
"--limit <n>",
|
|
1623
1678
|
`entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
|
|
1624
1679
|
activityLimit
|
|
@@ -1814,6 +1869,232 @@ relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--j
|
|
|
1814
1869
|
return relationRm(ctx, issue, idOrIssue);
|
|
1815
1870
|
});
|
|
1816
1871
|
});
|
|
1872
|
+
var plan = program.command("plan").description("plans: a whole graph of issues created in one request, then released and held");
|
|
1873
|
+
plan.command("create").description("create a plan from a JSON or YAML file (the API's request body); it starts held").option("--file <path>", 'the plan: .json is JSON, anything else YAML ("-" for stdin)').option("--team <key>", "team of the root and of people's steps (or HARNESS_TEAM)").option("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option("--json", "emit JSON").action(async (opts) => {
|
|
1874
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1875
|
+
if (opts.file === void 0) {
|
|
1876
|
+
throw new UsageError("usage: harness plan create --file <plan.json|plan.yaml> [--team <key>]");
|
|
1877
|
+
}
|
|
1878
|
+
const team2 = requiredTeam(ctx, opts.team);
|
|
1879
|
+
validateIdempotencyKey(opts.idempotencyKey);
|
|
1880
|
+
return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
|
|
1881
|
+
});
|
|
1882
|
+
});
|
|
1883
|
+
plan.command("show").description("a plan's status, budgets, revision, steps, edges and wait conditions").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
1884
|
+
await withConfig(opts.json === true, false, (ctx) => {
|
|
1885
|
+
if (id === void 0) throw new UsageError("usage: harness plan show <plan-id>");
|
|
1886
|
+
return planShow(ctx, id);
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[id]").option("--file <path>", 'the edit: .json is JSON, anything else YAML ("-" for stdin)').option("--json", "emit JSON").action(async (id, opts) => {
|
|
1890
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1891
|
+
if (id === void 0 || opts.file === void 0) {
|
|
1892
|
+
throw new UsageError("usage: harness plan edit <plan-id> --file <edits.json|edits.yaml>");
|
|
1893
|
+
}
|
|
1894
|
+
requirePlanId(id);
|
|
1895
|
+
return planEdit(ctx, id, readPlanFile(opts.file));
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
1898
|
+
plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[id]").option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer).option("--json", "emit JSON").action(
|
|
1899
|
+
async (id, opts) => {
|
|
1900
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1901
|
+
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id>");
|
|
1902
|
+
return planRelease(ctx, id, { ...opts, collaborators: opts.collaborator });
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
);
|
|
1906
|
+
plan.command("hold").description("pause every step of a plan until it is released again").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
|
|
1907
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1908
|
+
if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id>");
|
|
1909
|
+
return planHold(ctx, id);
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
var gate = program.command("gate").description("manual wait conditions: gate satisfy|unsatisfy <issue> <gate>");
|
|
1913
|
+
for (const [verb, satisfied] of [
|
|
1914
|
+
["satisfy", true],
|
|
1915
|
+
["unsatisfy", false]
|
|
1916
|
+
]) {
|
|
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) => {
|
|
1918
|
+
await withConfig(opts.json === true, true, (ctx) => {
|
|
1919
|
+
if (issue === void 0 || gateName === void 0) {
|
|
1920
|
+
throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
|
|
1921
|
+
}
|
|
1922
|
+
return gateSet(ctx, issue, gateName, satisfied);
|
|
1923
|
+
});
|
|
1924
|
+
});
|
|
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
|
+
);
|
|
1817
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(
|
|
1818
2099
|
async (id, text, opts) => {
|
|
1819
2100
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -2093,9 +2374,40 @@ program.command("upload").description("upload an image (25 MB) or H.264 MP4 vide
|
|
|
2093
2374
|
return upload(ctx, path);
|
|
2094
2375
|
});
|
|
2095
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
|
+
});
|
|
2096
2408
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2097
|
-
const { startMcpServer } = await import("./server-
|
|
2098
|
-
await startMcpServer();
|
|
2409
|
+
const { startMcpServer } = await import("./server-O7CX2ZCC.js");
|
|
2410
|
+
await startMcpServer(invokedAs);
|
|
2099
2411
|
});
|
|
2100
2412
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
2101
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) => {
|
|
@@ -2155,7 +2467,16 @@ program.command("whoami").description("show the resolved host and credential sou
|
|
|
2155
2467
|
return exitWith(2);
|
|
2156
2468
|
}
|
|
2157
2469
|
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
2158
|
-
|
|
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);
|
|
2159
2480
|
});
|
|
2160
2481
|
function storedLoginUrl() {
|
|
2161
2482
|
return readCredentials(defaultCredentialsPath())?.url;
|
|
@@ -2183,6 +2504,7 @@ try {
|
|
|
2183
2504
|
if (notice !== null) process.stderr.write(notice);
|
|
2184
2505
|
} catch {
|
|
2185
2506
|
}
|
|
2507
|
+
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
2186
2508
|
if (argv.length <= 2) {
|
|
2187
2509
|
program.outputHelp({ error: true });
|
|
2188
2510
|
await exitWith(2);
|