@allocator-one/harness-cli 1.15.1 → 1.16.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.
@@ -331,6 +331,7 @@ function issueDetail(issue, images = []) {
331
331
  ["Priority", issue["priority_label"]],
332
332
  ["Assignee", path(issue, "assignee", "name")],
333
333
  ["Team", path(issue, "team", "key")],
334
+ ["Plan", issuePlanLine(issue["plan"])],
334
335
  ["Labels", labelsLine(issue["labels"])],
335
336
  ["Blocked", blockedLine(issue)],
336
337
  ["Waiting", waitingLine(issue)],
@@ -735,6 +736,34 @@ function roleName(role) {
735
736
  return words.charAt(0).toUpperCase() + words.slice(1);
736
737
  }
737
738
  var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
739
+ function plansTable(plans) {
740
+ const rows = plans.map((plan) => [
741
+ str(plan["root"]) || "-",
742
+ str(plan["status"]) || "-",
743
+ plan["held"] === true ? "yes" : plan["held"] === false ? "no" : "-",
744
+ typeof plan["revision"] === "number" ? String(plan["revision"]) : "-",
745
+ truncate(plan["title"])
746
+ ]);
747
+ return table(["ROOT", "STATUS", "HELD", "REVISION", "TITLE"], rows);
748
+ }
749
+ function issuePlanLine(plan) {
750
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return null;
751
+ const record = plan;
752
+ const root = clean(record["root"]);
753
+ if (root === "") return null;
754
+ const status = record["held"] === true ? "held" : clean(record["status"]);
755
+ const revision = record["revision"];
756
+ const parts = [root];
757
+ if (status !== "") parts.push(status);
758
+ if (typeof revision === "number") {
759
+ parts.push(`revision ${revision}`);
760
+ if (typeof record["in_revision"] === "boolean") parts.push(record["in_revision"] ? "in revision" : "not in revision");
761
+ } else {
762
+ parts.push("never released");
763
+ }
764
+ if (record["executor"] === "operator") parts.push("Operator step");
765
+ return parts.join(" \xB7 ");
766
+ }
738
767
  function planDetail(plan) {
739
768
  const steps = records(plan["steps"]);
740
769
  const fields = [
@@ -863,7 +892,7 @@ function holderText(holder) {
863
892
  function planHeldSection(heldSteps) {
864
893
  if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
865
894
  return `
866
- Held by an edit until the plan is released again: ${heldSteps.map((step) => clean(step)).join(", ")}
895
+ Changed since the release: ${heldSteps.map((step) => clean(step)).join(", ")} (only Operator steps are held until the next release takes them in)
867
896
  `;
868
897
  }
869
898
  function indentLines(text) {
@@ -883,11 +912,32 @@ function changesetDetails(details) {
883
912
  function violationDetails(violations) {
884
913
  return records(violations).map((violation) => {
885
914
  const code = clean(violation["code"]);
886
- const message = clean(violation["message"]) || code || "invalid";
915
+ const message = capMessage(violation) ?? (clean(violation["message"]) || code || "invalid");
887
916
  return `
888
917
  ${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
889
918
  }).join("");
890
919
  }
920
+ var MAX_PLAN_STEPS_CEILING = 2e3;
921
+ function capMessage(violation) {
922
+ if (violation["code"] !== "too_many_steps") return null;
923
+ const cap = violation["cap"];
924
+ if (typeof cap !== "object" || cap === null || Array.isArray(cap)) return null;
925
+ const { kind, max, count, ceiling: served } = cap;
926
+ if (!Number.isInteger(max) || !Number.isInteger(count)) return null;
927
+ if (kind === "operator_steps") {
928
+ return `the plan has ${count} Operator steps, above the fixed limit of ${max}: Operator's delegated grant caps a plan's agent steps, and no setting raises it \u2014 hand some to people (executor none) or split the plan`;
929
+ }
930
+ if (kind === "plan_steps") {
931
+ const ceiling = Number.isInteger(served) ? served : MAX_PLAN_STEPS_CEILING;
932
+ const over = `the plan has ${count} steps, above this organization's max plan steps (${max})`;
933
+ if (count > ceiling && max < ceiling) {
934
+ return `${over}, and above the ceiling of ${ceiling} that no setting raises past \u2014 split the plan`;
935
+ }
936
+ if (max >= ceiling) return `${over}, which is already at its ceiling of ${ceiling} \u2014 split the plan`;
937
+ return `${over}: a Harness admin can raise "Max plan steps" in Harness settings \u2192 Plans (max_plan_steps, up to ${ceiling})`;
938
+ }
939
+ return null;
940
+ }
891
941
  function violationSubject(violation) {
892
942
  const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
893
943
  const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
@@ -1185,6 +1235,9 @@ var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
1185
1235
  function isUuid(value) {
1186
1236
  return UUID_FORMAT.test(value);
1187
1237
  }
1238
+ function isIdentifier(value) {
1239
+ return IDENTIFIER_FORMAT.test(value);
1240
+ }
1188
1241
  function requireIdentifiers(identifiers) {
1189
1242
  const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
1190
1243
  if (bad.length > 0) {
@@ -1514,18 +1567,42 @@ ${opts.appendNotes}`;
1514
1567
  return { json: issue, human };
1515
1568
  }
1516
1569
  var CLAIM_REFUSAL_HINTS = {
1517
- // The edit holds a released step, so the plan needs releasing again before
1518
- // the step is claimable.
1519
- agent_step: () => `Operator runs it for its plan; to do it yourself, set its executor to none (${invokedName()} plan edit <plan-id>) and release the plan again`,
1570
+ // A person never claims an Operator step, so releasing the plan is not the
1571
+ // fix: handing the step to a person is, and a handed-over Operator step
1572
+ // stays frozen until the next release.
1573
+ agent_step: (_identifier, plan) => {
1574
+ const runs = plan.kind === "sole" ? `plan ${plan.root}` : plan.kind === "nearest" ? `its plans (several govern it; the nearest is ${plan.root})` : "its plan";
1575
+ const target = plan.kind === "sole" ? plan.root : "<plan>";
1576
+ return `a person can't claim an Operator step: Operator runs it for ${runs}. To do it yourself, set its executor to none (${invokedName()} plan edit ${target}) and release the plan again`;
1577
+ },
1578
+ plan_not_released: (_identifier, plan) => planReleaseHint(plan),
1579
+ not_in_plan_revision: (_identifier, plan) => planReleaseHint(plan),
1580
+ edited_since_release: (_identifier, plan) => planReleaseHint(plan),
1520
1581
  waiting_on_condition: (identifier) => `${invokedName()} show ${identifier} lists what it waits on; tick a gate with ${invokedName()} gate satisfy ${identifier} <gate>`
1521
1582
  };
1583
+ function planReleaseHint(plan) {
1584
+ if (plan.kind === "sole") return plan.release ? `run ${invokedName()} plan release ${plan.root}` : void 0;
1585
+ if (plan.kind === "nearest") {
1586
+ return `several plans govern it; the nearest is ${plan.root} (${invokedName()} plan show ${plan.root})`;
1587
+ }
1588
+ return void 0;
1589
+ }
1522
1590
  function explainClaimRefusal(error, identifier) {
1523
1591
  if (!(error instanceof HttpError) || error.status !== 409) return error;
1524
1592
  const reason = error.body["reason"];
1525
- const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier) : void 0;
1593
+ const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier, refusingPlan(error.body)) : void 0;
1526
1594
  if (hint === void 0) return error;
1527
1595
  return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
1528
1596
  }
1597
+ function refusingPlan(body) {
1598
+ const plan = body["plan"];
1599
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return { kind: "none" };
1600
+ const root = plan["root"];
1601
+ if (typeof root !== "string" || !isIdentifier(root)) return { kind: "none" };
1602
+ const message = typeof body["message"] === "string" ? body["message"] : "";
1603
+ if (!new RegExp(`\\bplan ${root}\\b`).test(message)) return { kind: "nearest", root };
1604
+ return { kind: "sole", root, release: new RegExp(`\\brelease plan ${root}\\b`).test(message) };
1605
+ }
1529
1606
  function requireCloseArgs(identifiers, opts) {
1530
1607
  if (identifiers.length === 0) {
1531
1608
  throw new UsageError(
@@ -4273,6 +4350,9 @@ import { parseDocument } from "yaml";
4273
4350
  var PLAN_EXECUTORS = ["none", "operator"];
4274
4351
  var CONDITION_KINDS = ["not_before", "gate", "event"];
4275
4352
  var GATE_OWNER_ROLES = ["gp", "compliance_officer", "valuer", "accountant", "auditor", "counsel"];
4353
+ var PLAN_STATUSES = ["held", "released", "running", "completed", "needs_attention"];
4354
+ var PLAN_LIST_MAX_LIMIT = 100;
4355
+ var PLAN_LIST_DEFAULT_LIMIT = 50;
4276
4356
  function parsePlanDocument(text, source) {
4277
4357
  const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
4278
4358
  if (!isRecord5(parsed)) {
@@ -4335,13 +4415,44 @@ Nothing runs until it is released: ${invokedName()} plan release ${clean(id)}
4335
4415
  `
4336
4416
  };
4337
4417
  }
4418
+ function requirePlanListArgs(opts) {
4419
+ if (opts.status !== void 0 && !PLAN_STATUSES.includes(opts.status)) {
4420
+ throw new UsageError(`status must be one of: ${PLAN_STATUSES.join(", ")}`);
4421
+ }
4422
+ if (opts.limit !== void 0 && (!Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > PLAN_LIST_MAX_LIMIT)) {
4423
+ throw new UsageError(`--limit takes 1 to ${PLAN_LIST_MAX_LIMIT}: the server returns at most ${PLAN_LIST_MAX_LIMIT} plans`);
4424
+ }
4425
+ }
4426
+ async function planList(ctx, opts) {
4427
+ requirePlanListArgs(opts);
4428
+ const team = requiredTeam(ctx, opts.team);
4429
+ let body;
4430
+ try {
4431
+ body = await ctx.client.get(`/api/v1/teams/${team}/plans`, { status: opts.status, limit: opts.limit });
4432
+ } catch (error) {
4433
+ throw explainMissing(error, `no team ${team} here, or this Harness server cannot list plans yet`);
4434
+ }
4435
+ if (!Array.isArray(body.data)) throw new CommandError("The plan list response carried no list.");
4436
+ const list3 = body.data.filter(isRecord5);
4437
+ const limit = opts.limit ?? PLAN_LIST_DEFAULT_LIMIT;
4438
+ let notice;
4439
+ if (list3.length >= limit) {
4440
+ notice = limit < PLAN_LIST_MAX_LIMIT ? `harness: showing the newest ${list3.length} plans \u2014 pass --limit (up to ${PLAN_LIST_MAX_LIMIT}) for older ones.
4441
+ ` : `harness: showing the newest ${list3.length} plans, the most one list returns \u2014 narrow it with --status.
4442
+ `;
4443
+ }
4444
+ const what = opts.status === void 0 ? "plans" : `${opts.status} plans`;
4445
+ const human = list3.length === 0 ? `No ${what} in team ${team}.
4446
+ ` : plansTable(list3);
4447
+ return notice === void 0 ? { json: body.data, human } : { json: body.data, human, notice };
4448
+ }
4338
4449
  async function planShow(ctx, id) {
4339
- requirePlanId(id);
4450
+ requirePlanRef(id);
4340
4451
  const plan = await planRequest(id, () => ctx.client.get(`/api/v1/plans/${id}`));
4341
4452
  return { json: plan, human: planDetail(plan) };
4342
4453
  }
4343
4454
  async function planEdit(ctx, id, edits) {
4344
- requirePlanId(id);
4455
+ requirePlanRef(id);
4345
4456
  requireDocument(edits, "a plan edit is one object: steps to change and budgets");
4346
4457
  const plan = await planRequest(id, () => ctx.client.patch(`/api/v1/plans/${id}`, edits));
4347
4458
  const held = Array.isArray(plan["held_steps"]) && plan["held_steps"].length > 0;
@@ -4350,12 +4461,12 @@ async function planEdit(ctx, id, edits) {
4350
4461
  human: `Updated plan ${clean(id)}
4351
4462
 
4352
4463
  ` + planDetail(plan) + (held ? `
4353
- Release it again to run the held steps: ${invokedName()} plan release ${clean(id)}
4464
+ Only Operator steps are held until the next release; release the plan again to take in the changes: ${invokedName()} plan release ${clean(id)}
4354
4465
  ` : "")
4355
4466
  };
4356
4467
  }
4357
4468
  async function planRelease(ctx, id, opts) {
4358
- requirePlanId(id);
4469
+ requirePlanRef(id);
4359
4470
  const body = {};
4360
4471
  if (opts.collaborators !== void 0 && opts.collaborators.length > 0) body["collaborators"] = opts.collaborators;
4361
4472
  if (opts.retryBudget !== void 0) body["retry_budget"] = opts.retryBudget;
@@ -4370,7 +4481,7 @@ async function planRelease(ctx, id, opts) {
4370
4481
  };
4371
4482
  }
4372
4483
  async function planHold(ctx, id) {
4373
- requirePlanId(id);
4484
+ requirePlanRef(id);
4374
4485
  const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/hold`));
4375
4486
  return {
4376
4487
  json: plan,
@@ -4383,25 +4494,41 @@ async function gateSet(ctx, issue, gate, satisfied) {
4383
4494
  const wanted = gate.trim();
4384
4495
  if (wanted === "") throw new UsageError("name the gate by its key, its label or its condition id");
4385
4496
  const expert = isExpertToken(ctx.config.token);
4386
- const list3 = await readConditions(ctx, issue, expert);
4387
- const target = findGate(list3, issue, wanted, expert);
4497
+ let list3 = null;
4498
+ if (!isUuid(wanted)) {
4499
+ try {
4500
+ list3 = await readConditions(ctx, issue, expert);
4501
+ } catch (error) {
4502
+ if (error instanceof HttpError && error.status === 403) {
4503
+ throw new HttpError(
4504
+ error.status,
4505
+ `${error.message} \u2014 find the gate by its condition id; only Harness staff can list an issue's conditions`,
4506
+ error.body,
4507
+ error.retryAfter
4508
+ );
4509
+ }
4510
+ throw error;
4511
+ }
4512
+ }
4513
+ const target = list3 === null ? { id: wanted.toLowerCase(), label: wanted.toLowerCase() } : findGate(list3, issue, wanted, expert);
4388
4514
  const id = str7(target["id"]);
4389
4515
  let body;
4390
4516
  try {
4391
4517
  body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
4392
4518
  } catch (error) {
4393
- throw await explainNotGateOwner(ctx, error, target, issue, satisfied);
4519
+ throw await explainNotGateOwner(ctx, error, target, issue, satisfied, list3 !== null);
4394
4520
  }
4395
- const others = list3.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length;
4521
+ const others = list3?.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length ?? 0;
4396
4522
  let tail = "";
4397
- if (satisfied && expert) {
4523
+ if (satisfied && list3 !== null && expert) {
4398
4524
  tail = others > 0 ? ` \u2014 ${others} other gate${others === 1 ? "" : "s"} of yours on it ${others === 1 ? "is" : "are"} open` : "";
4399
- } else if (satisfied) {
4525
+ } else if (satisfied && list3 !== null) {
4400
4526
  tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
4401
4527
  }
4528
+ const label = list3 === null && isRecord5(body.data) ? body.data["label"] ?? id : target["label"];
4402
4529
  return {
4403
4530
  json: body.data,
4404
- human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(target["label"])}" on ${issue}${tail}
4531
+ human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(label)}" on ${issue}${tail}
4405
4532
  `
4406
4533
  };
4407
4534
  }
@@ -4435,8 +4562,10 @@ async function readConditions(ctx, issue, expert) {
4435
4562
  if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
4436
4563
  return conditions.filter(isRecord5);
4437
4564
  }
4438
- function requirePlanId(id) {
4439
- if (!isUuid(id)) throw new UsageError(`not a plan id: ${id} (expected the plan's UUID)`);
4565
+ function requirePlanRef(id) {
4566
+ if (!isUuid(id) && !isIdentifier(id)) {
4567
+ throw new UsageError(`not a plan: ${id} (expected the plan's id or its root issue's identifier, e.g. FR-1)`);
4568
+ }
4440
4569
  }
4441
4570
  function requireDocument(value, what) {
4442
4571
  if (!isRecord5(value)) throw new UsageError(what);
@@ -4445,18 +4574,21 @@ async function planRequest(id, send) {
4445
4574
  try {
4446
4575
  return (await send()).data;
4447
4576
  } catch (error) {
4448
- throw explainMissing(error, `no plan ${id} in this organization, or this Harness server has no plan API yet`);
4577
+ throw explainMissing(
4578
+ error,
4579
+ isUuid(id) ? `no plan ${id} in this organization, or this Harness server has no plan API yet` : `${id} roots no plan in this organization (${invokedName()} show ${id} names the plan an issue belongs to, by its root), or this Harness server opens plans by their id only`
4580
+ );
4449
4581
  }
4450
4582
  }
4451
4583
  function explainMissing(error, why) {
4452
4584
  if (!(error instanceof HttpError) || error.status !== 404) return error;
4453
4585
  return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
4454
4586
  }
4455
- async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
4587
+ async function explainNotGateOwner(ctx, error, gate, issue, satisfied, allowLookup) {
4456
4588
  if (!(error instanceof HttpError) || error.status !== 403 || error.body["reason"] !== "not_gate_owner") return error;
4457
4589
  let current;
4458
4590
  let currentOwner = error.body["owner"];
4459
- if (currentOwner !== null && !isRecord5(currentOwner)) {
4591
+ if (allowLookup && currentOwner !== null && !isRecord5(currentOwner)) {
4460
4592
  try {
4461
4593
  const fresh = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
4462
4594
  if (Array.isArray(fresh)) current = fresh.filter(isRecord5).find((condition) => condition["id"] === gate["id"]);
@@ -4473,23 +4605,17 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
4473
4605
  var gateLabel = (condition) => str7(condition["label"]).trim();
4474
4606
  var gateKey = (condition) => str7(condition["key"]);
4475
4607
  function exactMatches(conditions, wanted) {
4476
- const id = isUuid(wanted) ? wanted.toLowerCase() : null;
4477
- return conditions.filter((condition) => {
4478
- if (id !== null && str7(condition["id"]).toLowerCase() === id) return true;
4479
- if (condition["kind"] !== "gate") return false;
4480
- const key = gateKey(condition);
4481
- return key !== "" && key === (id ?? wanted) || gateLabel(condition) === wanted;
4482
- });
4608
+ return conditions.filter(
4609
+ (condition) => condition["kind"] === "gate" && (gateKey(condition) !== "" && gateKey(condition) === wanted || gateLabel(condition) === wanted)
4610
+ );
4483
4611
  }
4484
4612
  function ambiguous(conditions, issue, wanted, matches) {
4485
4613
  const listed = matches.map((condition) => {
4486
4614
  const what = condition["kind"] === "gate" ? `gate ${gateName({ label: gateLabel(condition), key: gateKey(condition) })}` : `${clean(condition["kind"])} condition`;
4487
4615
  return `${clean(condition["id"])} (${what})`;
4488
4616
  }).join(", ");
4489
- const unnameable = matches.map((condition) => str7(condition["id"])).filter((id) => exactMatches(conditions, id).length !== 1).map((id) => clean(id));
4490
- const caveat = unnameable.length === 0 ? "" : ` (${unnameable.join(", ")} ${unnameable.length === 1 ? "is" : "are"} also another gate's key or label, so ${unnameable.length === 1 ? "it" : "they"} cannot be named by id)`;
4491
4617
  return new CommandError(
4492
- `several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean${caveat}`
4618
+ `several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean`
4493
4619
  );
4494
4620
  }
4495
4621
  function findGate(conditions, issue, wanted, expert = false) {
@@ -4497,11 +4623,6 @@ function findGate(conditions, issue, wanted, expert = false) {
4497
4623
  if (exact.length > 1) throw ambiguous(conditions, issue, wanted, exact);
4498
4624
  if (exact.length === 1) {
4499
4625
  const only = exact[0];
4500
- if (only["kind"] !== "gate") {
4501
- throw new CommandError(
4502
- `condition ${wanted} on ${issue} is a ${clean(only["kind"])} condition \u2014 only a gate is satisfied by hand`
4503
- );
4504
- }
4505
4626
  return only;
4506
4627
  }
4507
4628
  const gates = conditions.filter((condition) => condition["kind"] === "gate");
@@ -4511,7 +4632,6 @@ function findGate(conditions, issue, wanted, expert = false) {
4511
4632
  );
4512
4633
  if (loose.length > 1) throw ambiguous(conditions, issue, wanted, loose);
4513
4634
  if (loose.length === 1) return loose[0];
4514
- if (isUuid(wanted)) throw new CommandError(`No condition ${wanted} on ${issue}${expert ? " that you own" : ""}.`);
4515
4635
  const none = expert ? `No gate of yours on ${issue}` : `No gate on ${issue}`;
4516
4636
  if (gates.length === 0) {
4517
4637
  throw new CommandError(`${none} matches "${wanted}" \u2014 ${expert ? "you own no gate on it" : "it has no gates"}`);
@@ -4544,6 +4664,7 @@ function isRecord6(value) {
4544
4664
  }
4545
4665
 
4546
4666
  // src/core/credentials.ts
4667
+ import { createHash } from "crypto";
4547
4668
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4548
4669
  import { homedir } from "os";
4549
4670
  import { dirname as dirname2, isAbsolute, join as join2 } from "path";
@@ -4552,6 +4673,15 @@ function defaultCredentialsPath(env = process.env) {
4552
4673
  const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
4553
4674
  return join2(base, "harness", "credentials.json");
4554
4675
  }
4676
+ function orgCredentialsPath(path2, baseUrl, org) {
4677
+ const url = new URL(baseUrl);
4678
+ const host = url.origin + url.pathname.replace(/\/+$/, "");
4679
+ const key = createHash("sha256").update(JSON.stringify([path2, host, org.toLowerCase()])).digest("hex");
4680
+ return join2(dirname2(path2), `credentials-org-${key}.json`);
4681
+ }
4682
+ function validOrgId(org) {
4683
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(org);
4684
+ }
4555
4685
  function readCredentials(path2) {
4556
4686
  try {
4557
4687
  const parsed = JSON.parse(readFileSync3(path2, "utf8"));
@@ -4666,6 +4796,10 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
4666
4796
  const envUrl = blankToNull(env["HARNESS_API_URL"]);
4667
4797
  const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
4668
4798
  const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
4799
+ const org = envToken === null ? repo?.org?.toLowerCase() : void 0;
4800
+ if (org !== void 0 && !validOrgId(org)) {
4801
+ return { kind: "error", message: `repo org in ${repo?.path} must be an organization UUID`, warnings };
4802
+ }
4669
4803
  let rawUrl;
4670
4804
  let hostSource;
4671
4805
  if (envUrl !== null) {
@@ -4684,6 +4818,9 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
4684
4818
  } else if (repoUrl !== null) {
4685
4819
  rawUrl = repoUrl;
4686
4820
  hostSource = "repo-config";
4821
+ } else if (org !== void 0) {
4822
+ rawUrl = DEFAULT_BASE_URL;
4823
+ hostSource = "default";
4687
4824
  } else if (stored !== null) {
4688
4825
  rawUrl = stored.url;
4689
4826
  hostSource = "stored-login";
@@ -4694,13 +4831,16 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
4694
4831
  const validated = validateBaseUrl(rawUrl);
4695
4832
  if (!validated.ok) return { kind: "error", message: validated.message, warnings };
4696
4833
  const baseUrl = rawUrl.replace(/\/+$/, "");
4834
+ const selectedPath = org === void 0 ? path2 : orgCredentialsPath(path2, baseUrl, org);
4835
+ const selected = org === void 0 ? stored : readCredentials(selectedPath);
4697
4836
  let token;
4698
4837
  if (envToken !== null) {
4699
4838
  token = envToken;
4700
- } else if (stored !== null && normalize(stored.url) !== null && normalize(stored.url) === normalize(baseUrl)) {
4701
- token = stored.token;
4839
+ } else if (selected !== null && normalize(selected.url) !== null && normalize(selected.url) === normalize(baseUrl)) {
4840
+ token = selected.token;
4702
4841
  } else {
4703
- const result = { kind: "needs-login", baseUrl, hostSource, warnings };
4842
+ const result = { kind: "needs-login", baseUrl, hostSource, credentialsPath: selectedPath, warnings };
4843
+ if (org !== void 0) result.org = org;
4704
4844
  if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
4705
4845
  return result;
4706
4846
  }
@@ -4714,7 +4854,8 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
4714
4854
  // the --team flag at the command layer) stays authoritative.
4715
4855
  team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
4716
4856
  cacheDir: resolveCacheDir(env),
4717
- hostSource
4857
+ hostSource,
4858
+ credentialsPath: selectedPath
4718
4859
  },
4719
4860
  warnings
4720
4861
  };
@@ -4753,10 +4894,11 @@ function validateBaseUrl(baseUrl) {
4753
4894
  }
4754
4895
  if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
4755
4896
  if (parsed.protocol === "http:") {
4756
- if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname)) return { ok: true };
4897
+ const host = parsed.hostname;
4898
+ if (["localhost", "127.0.0.1", "[::1]"].includes(host) || host.endsWith(".localhost")) return { ok: true };
4757
4899
  return {
4758
4900
  ok: false,
4759
- message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost only.`
4901
+ message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost, *.localhost, 127.0.0.1 or [::1] only.`
4760
4902
  };
4761
4903
  }
4762
4904
  return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
@@ -4913,17 +5055,23 @@ export {
4913
5055
  PLAN_EXECUTORS,
4914
5056
  CONDITION_KINDS,
4915
5057
  GATE_OWNER_ROLES,
5058
+ PLAN_STATUSES,
5059
+ PLAN_LIST_MAX_LIMIT,
4916
5060
  readPlanFile,
4917
5061
  planCreate,
5062
+ requirePlanListArgs,
5063
+ planList,
4918
5064
  planShow,
4919
5065
  planEdit,
4920
5066
  planRelease,
4921
5067
  planHold,
4922
5068
  gateSet,
4923
5069
  conditionList,
4924
- requirePlanId,
5070
+ requirePlanRef,
4925
5071
  assignments,
4926
5072
  defaultCredentialsPath,
5073
+ orgCredentialsPath,
5074
+ validOrgId,
4927
5075
  readCredentials,
4928
5076
  writeCredentials,
4929
5077
  deleteCredentials,