@indigoai-us/hq-cli 5.50.2 → 5.52.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/bin/hq-auth-refresh.d.ts +1 -1
  3. package/dist/bin/hq-auth-refresh.js +5 -2
  4. package/dist/commands/files.js +33 -3
  5. package/dist/commands/members.d.ts +27 -0
  6. package/dist/commands/members.js +95 -37
  7. package/dist/commands/people.d.ts +26 -1
  8. package/dist/commands/people.js +70 -7
  9. package/dist/commands/secrets-scope.d.ts +20 -0
  10. package/dist/commands/secrets-scope.js +19 -0
  11. package/dist/commands/secrets.js +21 -6
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +44 -14
  14. package/dist/node-preflight.d.ts +39 -0
  15. package/dist/node-preflight.js +55 -0
  16. package/dist/sentry.d.ts +12 -0
  17. package/dist/sentry.js +19 -3
  18. package/dist/utils/epipe.d.ts +8 -0
  19. package/dist/utils/epipe.js +30 -0
  20. package/dist/utils/intercepted-process-exit.d.ts +7 -0
  21. package/dist/utils/intercepted-process-exit.js +38 -0
  22. package/e2e/cli.test.ts +35 -0
  23. package/package.json +1 -1
  24. package/src/bin/hq-auth-refresh.ts +3 -0
  25. package/src/commands/files.test.ts +130 -0
  26. package/src/commands/files.ts +55 -3
  27. package/src/commands/members.test.ts +292 -0
  28. package/src/commands/members.ts +153 -43
  29. package/src/commands/people.test.ts +212 -5
  30. package/src/commands/people.ts +141 -5
  31. package/src/commands/secrets-scope.test.ts +56 -0
  32. package/src/commands/secrets-scope.ts +32 -0
  33. package/src/commands/secrets.ts +24 -10
  34. package/src/index.ts +40 -12
  35. package/src/node-preflight.test.ts +60 -0
  36. package/src/node-preflight.ts +67 -0
  37. package/src/sentry-epipe.test.ts +37 -0
  38. package/src/sentry-release.test.ts +54 -0
  39. package/src/sentry.ts +21 -1
  40. package/src/utils/epipe.test.ts +28 -0
  41. package/src/utils/epipe.ts +29 -0
  42. package/src/utils/intercepted-process-exit.test.ts +37 -0
  43. package/src/utils/intercepted-process-exit.ts +36 -0
@@ -34,10 +34,12 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
34
34
  import { getCompanyUid } from "../utils/vault-api.js";
35
35
  import {
36
36
  InviteHttpError,
37
+ changeMemberRole,
37
38
  detectTarget,
38
39
  formatInviteHttpError,
39
40
  getCallerPersonUid,
40
41
  inviteMember,
42
+ listActiveMembers,
41
43
  listPendingInvites,
42
44
  registerMembersCommand,
43
45
  resendInvite,
@@ -743,6 +745,181 @@ describe("listPendingInvites", () => {
743
745
  });
744
746
  });
745
747
 
748
+ // ---------------------------------------------------------------------------
749
+ // listActiveMembers
750
+ // ---------------------------------------------------------------------------
751
+
752
+ describe("listActiveMembers", () => {
753
+ it("GETs /membership/company/{uid} and returns the members array", async () => {
754
+ fetchSpy.mockResolvedValueOnce(
755
+ jsonResponse(200, {
756
+ members: [
757
+ {
758
+ membershipKey: "k1",
759
+ personUid: "prs_alice",
760
+ companyUid: "cmp_acme",
761
+ role: "owner",
762
+ status: "active",
763
+ personEmail: "alice@example.com",
764
+ personName: "Alice",
765
+ },
766
+ ],
767
+ }),
768
+ );
769
+
770
+ const members = await listActiveMembers("test-token", "cmp_acme");
771
+
772
+ const call = fetchSpy.mock.calls[0];
773
+ expect(String(call[0])).toMatch(/\/membership\/company\/cmp_acme$/);
774
+ expect(members).toHaveLength(1);
775
+ expect(members[0].personEmail).toBe("alice@example.com");
776
+ expect(members[0].role).toBe("owner");
777
+ });
778
+
779
+ it("returns [] when the members key is missing", async () => {
780
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
781
+ await expect(
782
+ listActiveMembers("test-token", "cmp_acme"),
783
+ ).resolves.toEqual([]);
784
+ });
785
+
786
+ it("returns [] when members is null", async () => {
787
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { members: null }));
788
+ await expect(
789
+ listActiveMembers("test-token", "cmp_acme"),
790
+ ).resolves.toEqual([]);
791
+ });
792
+
793
+ it("throws InviteHttpError on non-ok", async () => {
794
+ fetchSpy.mockResolvedValueOnce(jsonResponse(403, { error: "forbidden" }));
795
+ await expect(
796
+ listActiveMembers("test-token", "cmp_acme"),
797
+ ).rejects.toBeInstanceOf(InviteHttpError);
798
+ });
799
+ });
800
+
801
+ // ---------------------------------------------------------------------------
802
+ // registerMembersCommand list
803
+ // ---------------------------------------------------------------------------
804
+
805
+ describe("registerMembersCommand list", () => {
806
+ it("defaults to ACTIVE members: renders EMAIL/ROLE/NAME + the share hint", async () => {
807
+ fetchSpy.mockResolvedValueOnce(
808
+ jsonResponse(200, {
809
+ members: [
810
+ {
811
+ membershipKey: "k1",
812
+ personUid: "prs_alice",
813
+ companyUid: "cmp_acme",
814
+ role: "owner",
815
+ status: "active",
816
+ personEmail: "alice@example.com",
817
+ personName: "Alice",
818
+ },
819
+ {
820
+ membershipKey: "k2",
821
+ personUid: "prs_bob",
822
+ companyUid: "cmp_acme",
823
+ role: "member",
824
+ status: "active",
825
+ personSlug: "bob",
826
+ },
827
+ ],
828
+ }),
829
+ );
830
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
831
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
832
+
833
+ await buildMembersProgram().parseAsync(
834
+ ["members", "--company", "acme", "list"],
835
+ { from: "user" },
836
+ );
837
+
838
+ const call = fetchSpy.mock.calls[0];
839
+ expect(String(call[0])).toMatch(/\/membership\/company\/cmp_acme$/);
840
+ const output = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
841
+ expect(output).toContain("EMAIL");
842
+ expect(output).toContain("ROLE");
843
+ expect(output).toContain("NAME");
844
+ expect(output).toContain("alice@example.com");
845
+ expect(output).toContain("owner");
846
+ expect(output).toContain("Alice");
847
+ // unresolved email falls back to personUid; name falls back to slug
848
+ expect(output).toContain("prs_bob");
849
+ expect(output).toContain("bob");
850
+ expect(output).toContain(
851
+ "hq secrets share <path> --with <email>",
852
+ );
853
+ expect(errSpy).not.toHaveBeenCalled();
854
+ });
855
+
856
+ it("default active: empty roster prints the no-active-members message", async () => {
857
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { members: [] }));
858
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
859
+
860
+ await buildMembersProgram().parseAsync(
861
+ ["members", "--company", "acme", "list"],
862
+ { from: "user" },
863
+ );
864
+
865
+ expect(logSpy).toHaveBeenCalledWith(
866
+ expect.stringContaining("No active members found for this company."),
867
+ );
868
+ });
869
+
870
+ it("--pending preserves the OLD pending-invites table verbatim", async () => {
871
+ fetchSpy.mockResolvedValueOnce(
872
+ jsonResponse(200, {
873
+ pending: [
874
+ {
875
+ membershipKey: "email:alice@example.com#cmp_acme",
876
+ inviteeEmail: "alice@example.com",
877
+ companyUid: "cmp_acme",
878
+ role: "member",
879
+ status: "pending",
880
+ invitedBy: "prs_admin",
881
+ invitedAt: "2026-05-21T12:00:00Z",
882
+ },
883
+ ],
884
+ }),
885
+ );
886
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
887
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
888
+
889
+ await buildMembersProgram().parseAsync(
890
+ ["members", "--company", "acme", "list", "--pending"],
891
+ { from: "user" },
892
+ );
893
+
894
+ const call = fetchSpy.mock.calls[0];
895
+ expect(String(call[0])).toMatch(
896
+ /\/membership\/company\/cmp_acme\/pending$/,
897
+ );
898
+ const output = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
899
+ expect(output).toContain("TARGET");
900
+ expect(output).toContain("INVITED_BY");
901
+ expect(output).toContain("MEMBERSHIP_KEY");
902
+ expect(output).toContain("alice@example.com");
903
+ // the active-only share hint must NOT appear in the pending view
904
+ expect(output).not.toContain("hq secrets share");
905
+ expect(errSpy).not.toHaveBeenCalled();
906
+ });
907
+
908
+ it("--pending: empty invites prints the no-pending-invites message", async () => {
909
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, { pending: [] }));
910
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
911
+
912
+ await buildMembersProgram().parseAsync(
913
+ ["members", "--company", "acme", "list", "--pending"],
914
+ { from: "user" },
915
+ );
916
+
917
+ expect(logSpy).toHaveBeenCalledWith(
918
+ expect.stringContaining("No pending invites for this company."),
919
+ );
920
+ });
921
+ });
922
+
746
923
  // ---------------------------------------------------------------------------
747
924
  // revokeInvite
748
925
  // ---------------------------------------------------------------------------
@@ -822,6 +999,121 @@ describe("registerMembersCommand set-role", () => {
822
999
  });
823
1000
  });
824
1001
 
1002
+ // ---------------------------------------------------------------------------
1003
+ // changeMemberRole + the unified `promote` command (alias `set-role`)
1004
+ // ---------------------------------------------------------------------------
1005
+
1006
+ describe("changeMemberRole", () => {
1007
+ it("rejects an invalid role WITHOUT making a network call", async () => {
1008
+ await expect(
1009
+ changeMemberRole(
1010
+ "test-token",
1011
+ "cmp_acme",
1012
+ "prs_x#cmp_acme",
1013
+ "superuser" as never,
1014
+ ),
1015
+ ).rejects.toThrow("must be one of owner, admin, member, guest");
1016
+ expect(fetchSpy).not.toHaveBeenCalled();
1017
+ });
1018
+
1019
+ it("accepts the full role set and POSTs to /membership/role", async () => {
1020
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
1021
+ await changeMemberRole("test-token", "cmp_acme", "prs_x#cmp_acme", "guest");
1022
+ const call = fetchSpy.mock.calls[0];
1023
+ expect(String(call[0])).toMatch(/\/membership\/role$/);
1024
+ const body = JSON.parse((call[1]?.body as string) ?? "{}");
1025
+ expect(body).toEqual({
1026
+ companyUid: "cmp_acme",
1027
+ membershipKey: "prs_x#cmp_acme",
1028
+ newRole: "guest",
1029
+ });
1030
+ });
1031
+ });
1032
+
1033
+ describe("registerMembersCommand promote", () => {
1034
+ it("resolves an email target and promotes to a full-set role the old set-role couldn't (admin)", async () => {
1035
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
1036
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
1037
+
1038
+ await buildMembersProgram().parseAsync(
1039
+ ["members", "--company", "acme", "promote", "alice@example.com", "admin"],
1040
+ { from: "user" },
1041
+ );
1042
+
1043
+ const call = fetchSpy.mock.calls[0];
1044
+ expect(String(call[0])).toMatch(/\/membership\/role$/);
1045
+ const body = JSON.parse((call[1]?.body as string) ?? "{}");
1046
+ expect(body).toEqual({
1047
+ companyUid: "cmp_acme",
1048
+ membershipKey: "email:alice@example.com#cmp_acme",
1049
+ newRole: "admin",
1050
+ });
1051
+ expect(logSpy).toHaveBeenCalledWith(
1052
+ expect.stringContaining("Updated role for 'alice@example.com' to admin"),
1053
+ );
1054
+ });
1055
+
1056
+ it("forwards a personUid target and the guest role (beyond set-role's admin|member cap)", async () => {
1057
+ fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
1058
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
1059
+
1060
+ await buildMembersProgram().parseAsync(
1061
+ ["members", "--company", "acme", "promote", "prs_bob", "guest"],
1062
+ { from: "user" },
1063
+ );
1064
+
1065
+ const body = JSON.parse(
1066
+ (fetchSpy.mock.calls[0]?.[1]?.body as string) ?? "{}",
1067
+ );
1068
+ expect(body).toEqual({
1069
+ companyUid: "cmp_acme",
1070
+ membershipKey: "prs_bob#cmp_acme",
1071
+ newRole: "guest",
1072
+ });
1073
+ });
1074
+
1075
+ it("rejects an invalid role at the CLI and exits non-zero without a network call", async () => {
1076
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
1077
+ vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
1078
+ throw new Error(`__EXIT__:${code ?? 0}`);
1079
+ }) as never);
1080
+
1081
+ await expect(
1082
+ buildMembersProgram().parseAsync(
1083
+ ["members", "--company", "acme", "promote", "alice@example.com", "wizard"],
1084
+ { from: "user" },
1085
+ ),
1086
+ ).rejects.toThrow("__EXIT__:1");
1087
+
1088
+ expect(errSpy).toHaveBeenCalledWith(
1089
+ expect.stringContaining("must be one of owner, admin, member, guest"),
1090
+ );
1091
+ expect(fetchSpy).not.toHaveBeenCalled();
1092
+ });
1093
+
1094
+ it("surfaces a backend 403 verbatim (server-enforced authorization)", async () => {
1095
+ fetchSpy.mockResolvedValueOnce(
1096
+ jsonResponse(403, { error: "Only an owner may promote a member to owner" }),
1097
+ );
1098
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
1099
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
1100
+ vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
1101
+ throw new Error(`__EXIT__:${code ?? 0}`);
1102
+ }) as never);
1103
+
1104
+ await expect(
1105
+ buildMembersProgram().parseAsync(
1106
+ ["members", "--company", "acme", "promote", "prs_bob", "owner"],
1107
+ { from: "user" },
1108
+ ),
1109
+ ).rejects.toThrow("__EXIT__:1");
1110
+
1111
+ expect(errSpy).toHaveBeenCalledWith(
1112
+ expect.stringContaining("Only an owner may promote a member to owner"),
1113
+ );
1114
+ });
1115
+ });
1116
+
825
1117
  // ---------------------------------------------------------------------------
826
1118
  // resolveRevokeTargetToMembershipKey
827
1119
  // ---------------------------------------------------------------------------
@@ -6,10 +6,8 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
6
6
  const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
7
7
  const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
8
8
  export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
9
- const VALID_MEMBER_SET_ROLES = new Set(["admin", "member"]);
10
9
 
11
10
  export type Role = "owner" | "admin" | "member" | "guest";
12
- type MemberSetRole = "admin" | "member";
13
11
 
14
12
  export interface PendingInvite {
15
13
  membershipKey: string;
@@ -31,6 +29,23 @@ interface MyMembership {
31
29
  status: string;
32
30
  }
33
31
 
32
+ /**
33
+ * An ACTIVE member of a company, as returned by
34
+ * `GET /membership/company/{companyUid}`. The server filters to
35
+ * `status: "active"` and enriches each row with resolved person metadata
36
+ * (`personEmail` / `personName` / `personSlug`) when available.
37
+ */
38
+ export interface ActiveMember {
39
+ membershipKey: string;
40
+ personUid: string;
41
+ companyUid: string;
42
+ role: string;
43
+ status: string;
44
+ personEmail?: string;
45
+ personName?: string;
46
+ personSlug?: string;
47
+ }
48
+
34
49
  export interface InviteOptions {
35
50
  target: string;
36
51
  role: string;
@@ -374,6 +389,30 @@ export async function listPendingInvites(
374
389
  return data?.pending ?? data?.invites ?? [];
375
390
  }
376
391
 
392
+ export async function listActiveMembers(
393
+ token: string,
394
+ companyUid: string,
395
+ ): Promise<ActiveMember[]> {
396
+ const res = await vaultApiFetch({
397
+ token,
398
+ path: `/membership/company/${encodeURIComponent(companyUid)}`,
399
+ });
400
+ if (!res.ok) {
401
+ const err = (await res.json().catch(() => ({}))) as Record<string, string>;
402
+ throw new InviteHttpError(
403
+ res.status,
404
+ err.message ?? err.error ?? res.statusText,
405
+ err.code,
406
+ );
407
+ }
408
+ // Server schema: `{ members: [...] }` — active members only, enriched with
409
+ // resolved person metadata (personEmail / personName / personSlug).
410
+ const data = (await res.json()) as {
411
+ members?: ActiveMember[] | null;
412
+ };
413
+ return data?.members ?? [];
414
+ }
415
+
377
416
  /**
378
417
  * Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
379
418
  * the server requires. Accepts three input forms:
@@ -457,15 +496,24 @@ export async function revokeInvite(
457
496
  }
458
497
  }
459
498
 
460
- async function setMemberRole(
499
+ /**
500
+ * Change a member's role via `POST /membership/role`, accepting the FULL role
501
+ * set (owner|admin|member|guest). This is a GENERAL role change — it can promote
502
+ * OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
503
+ * promote-to-owner / change-an-owner) is enforced SERVER-side; this function
504
+ * only validates the role string locally and surfaces the server's error.
505
+ * Role string is validated BEFORE any network call so callers/tests can rely on
506
+ * a synchronous-shaped rejection for a bad role.
507
+ */
508
+ export async function changeMemberRole(
461
509
  token: string,
462
510
  companyUid: string,
463
511
  membershipKey: string,
464
- newRole: MemberSetRole,
512
+ newRole: Role,
465
513
  ): Promise<void> {
466
- if (!VALID_MEMBER_SET_ROLES.has(newRole)) {
514
+ if (!VALID_ROLES.has(newRole)) {
467
515
  throw new Error(
468
- `Invalid role '${newRole}': must be one of admin, member`,
516
+ `Invalid role '${newRole}': must be one of owner, admin, member, guest`,
469
517
  );
470
518
  }
471
519
 
@@ -491,23 +539,41 @@ export function registerMembersCommand(program: Command): void {
491
539
  .description("Manage company memberships and invites")
492
540
  .option("--company <slug>", "Company slug (resolves to companyUid)");
493
541
 
542
+ // Canonical role-change command. `promote` is the discoverable verb users
543
+ // reach for; `set-role` is kept as an alias for the older name. The route is a
544
+ // GENERAL role change, so the help text and success message are honest that it
545
+ // can demote as well as promote. Authorization is enforced server-side.
494
546
  members
495
- .command("set-role <membershipKey> <role>")
496
- .description("Change a member or agent role to admin or member")
497
- .action(async (membershipKey: string, role: string) => {
547
+ .command("promote <target> <newRole>")
548
+ .alias("set-role")
549
+ .description(
550
+ "Change a member's role (owner|admin|member|guest) — promotes OR demotes. " +
551
+ "<target> may be an email, a prs_ personUid, or a full membership key. " +
552
+ "Owner-or-admin only; setting a target to owner (or changing an owner) is owner-only. Server-enforced.",
553
+ )
554
+ .action(async (target: string, newRole: string) => {
498
555
  try {
556
+ const role = newRole.trim().toLowerCase();
557
+ if (!VALID_ROLES.has(role)) {
558
+ console.error(
559
+ chalk.red(
560
+ `Invalid role '${newRole}': must be one of owner, admin, member, guest`,
561
+ ),
562
+ );
563
+ process.exit(1);
564
+ }
565
+
499
566
  const token = await ensureCognitoToken();
500
567
  const companySlug = members.opts().company as string | undefined;
501
568
  const companyUid = await getCompanyUid(token, companySlug);
502
-
503
- await setMemberRole(
504
- token,
569
+ const membershipKey = resolveRevokeTargetToMembershipKey(
570
+ target,
505
571
  companyUid,
506
- membershipKey,
507
- role as MemberSetRole,
508
572
  );
573
+
574
+ await changeMemberRole(token, companyUid, membershipKey, role as Role);
509
575
  console.log(
510
- chalk.green(`Updated role for '${membershipKey}' to ${role}`),
576
+ chalk.green(`Updated role for '${target}' to ${role}`),
511
577
  );
512
578
  } catch (err) {
513
579
  if (err instanceof InviteHttpError) {
@@ -746,55 +812,99 @@ export function registerMembersCommand(program: Command): void {
746
812
 
747
813
  members
748
814
  .command("list")
749
- .description("List pending invites for the company")
750
- .action(async () => {
815
+ .description(
816
+ "List the company's active members (use --pending for pending invites)",
817
+ )
818
+ .option("--pending", "List pending invites instead of active members")
819
+ .action(async (opts: { pending?: boolean }) => {
751
820
  try {
752
821
  const token = await ensureCognitoToken();
753
822
  const companySlug = members.opts().company as string | undefined;
754
823
  const companyUid = await getCompanyUid(token, companySlug);
755
824
 
756
- const invites = await listPendingInvites(token, companyUid);
825
+ if (opts.pending) {
826
+ // --pending: preserve the original pending-invites view verbatim.
827
+ const invites = await listPendingInvites(token, companyUid);
828
+
829
+ if (invites.length === 0) {
830
+ console.log(chalk.gray("No pending invites for this company."));
831
+ return;
832
+ }
833
+
834
+ const targetW = Math.max(
835
+ 6,
836
+ ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length),
837
+ );
838
+ const roleW = Math.max(4, ...invites.map((i) => i.role.length));
839
+ const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
840
+ const keyW = Math.max(
841
+ 14,
842
+ ...invites.map((i) => i.membershipKey.length),
843
+ );
844
+ console.log(
845
+ chalk.bold(
846
+ [
847
+ "TARGET".padEnd(targetW),
848
+ "ROLE".padEnd(roleW),
849
+ "INVITED_BY".padEnd(byW),
850
+ "INVITED_AT",
851
+ "MEMBERSHIP_KEY".padEnd(keyW),
852
+ ].join(" "),
853
+ ),
854
+ );
855
+ for (const inv of invites) {
856
+ const target = inv.inviteeEmail ?? inv.personUid ?? "";
857
+ console.log(
858
+ [
859
+ target.padEnd(targetW),
860
+ inv.role.padEnd(roleW),
861
+ inv.invitedBy.padEnd(byW),
862
+ shortDate(inv.invitedAt),
863
+ inv.membershipKey.padEnd(keyW),
864
+ ].join(" "),
865
+ );
866
+ }
867
+ return;
868
+ }
869
+
870
+ // Default: list ACTIVE members so their emails drop straight into
871
+ // `hq secrets share <path> --with <email>`.
872
+ const activeMembers = await listActiveMembers(token, companyUid);
757
873
 
758
- if (invites.length === 0) {
759
- console.log(chalk.gray("No pending invites for this company."));
874
+ if (activeMembers.length === 0) {
875
+ console.log(chalk.gray("No active members found for this company."));
760
876
  return;
761
877
  }
762
878
 
763
- const targetW = Math.max(
764
- 6,
765
- ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length),
879
+ const emailW = Math.max(
880
+ 5,
881
+ ...activeMembers.map((m) => (m.personEmail ?? m.personUid).length),
766
882
  );
767
- const roleW = Math.max(4, ...invites.map((i) => i.role.length));
768
- const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
769
- const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
883
+ const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
770
884
  console.log(
771
885
  chalk.bold(
772
- [
773
- "TARGET".padEnd(targetW),
774
- "ROLE".padEnd(roleW),
775
- "INVITED_BY".padEnd(byW),
776
- "INVITED_AT",
777
- "MEMBERSHIP_KEY".padEnd(keyW),
778
- ].join(" "),
886
+ ["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW), "NAME"].join(" "),
779
887
  ),
780
888
  );
781
- for (const inv of invites) {
782
- const target = inv.inviteeEmail ?? inv.personUid ?? "";
889
+ for (const m of activeMembers) {
890
+ const email = m.personEmail ?? m.personUid;
891
+ const name = m.personName ?? m.personSlug ?? "";
783
892
  console.log(
784
- [
785
- target.padEnd(targetW),
786
- inv.role.padEnd(roleW),
787
- inv.invitedBy.padEnd(byW),
788
- shortDate(inv.invitedAt),
789
- inv.membershipKey.padEnd(keyW),
790
- ].join(" "),
893
+ [email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "),
791
894
  );
792
895
  }
896
+ console.log(
897
+ chalk.gray(
898
+ "Share a secret with a member: hq secrets share <path> --with <email>",
899
+ ),
900
+ );
793
901
  } catch (err) {
794
902
  if (err instanceof InviteHttpError) {
795
903
  const msg =
796
904
  err.status === 403
797
- ? "Not authorized — only admins and owners can list invites"
905
+ ? opts.pending
906
+ ? "Not authorized — only admins and owners can list invites"
907
+ : "Not authorized — only company members can list members"
798
908
  : formatInviteHttpError(err.status, err.message);
799
909
  console.error(chalk.red(msg));
800
910
  process.exit(1);