@allocator-one/harness-cli 1.15.0 → 1.15.1

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