@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.52.0]
6
+
7
+ ### Added
8
+
9
+ - **`hq members promote <target> <newRole>` (alias `set-role`) — change a
10
+ member's role from the CLI.** A discoverable runner for role changes via
11
+ `POST /membership/role`, replacing the undiscoverable, `admin|member`-only
12
+ `set-role` (now kept as an alias). Supports the full role set
13
+ (`owner|admin|member|guest`); `<target>` may be an email, a `prs_` personUid,
14
+ or a full membership key (same resolver as `revoke`). It is a GENERAL role
15
+ change — it can promote OR demote — and authorization (owner-or-admin; only
16
+ an owner may set a target to owner or change an owner) is enforced
17
+ server-side.
18
+ - **`hq files share --full` — glob-safe whole-vault access.** Granting prefixes
19
+ one at a time hit `PolicyBudgetExceeded` after a few (each prefix is a
20
+ distinct ARN in the member's DEFLATE-packed inline STS session policy), and
21
+ the intended single-`*` wildcard escape hatch was unreachable because an
22
+ unquoted `*` expands to local filenames and fails the one-prefix check.
23
+ `--full` is a glob-safe flag that performs the coalesced whole-vault wildcard
24
+ grant in a single policy entry.
25
+
26
+ ## [5.51.0]
27
+
5
28
  ### Fixed
6
29
 
7
30
  - **`hq secrets exec` / `hq secrets env` load via the batch endpoint, killing
@@ -10,5 +10,5 @@
10
10
  * machine-token mint). Exit 1 if no valid session could be ensured
11
11
  * non-interactively.
12
12
  */
13
- export {};
13
+ import "../node-preflight.js";
14
14
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -10,8 +10,11 @@
10
10
  * machine-token mint). Exit 1 if no valid session could be ensured
11
11
  * non-interactively.
12
12
  */
13
+ // MUST be first: guard the Node version before any dependency that needs a
14
+ // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
13
15
 
14
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="03d2c531-f620-5d93-a87b-9a87264ad77b")}catch(e){}}();
16
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3c7e1b4e-66dc-5fe8-b828-cd3636df1fbb")}catch(e){}}();
17
+ import "../node-preflight.js";
15
18
  import { initSentry, Sentry } from "../sentry.js";
16
19
  import { refreshCachedSession } from "../utils/cognito-session.js";
17
20
  initSentry();
@@ -38,4 +41,4 @@ initSentry();
38
41
  }
39
42
  })();
40
43
  //# sourceMappingURL=hq-auth-refresh.js.map
41
- //# debugId=03d2c531-f620-5d93-a87b-9a87264ad77b
44
+ //# debugId=3c7e1b4e-66dc-5fe8-b828-cd3636df1fbb
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fc999359-c426-5277-ba96-5452ae1ed2e2")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import open from "open";
5
5
  import * as readline from "node:readline";
@@ -99,10 +99,35 @@ export function registerFilesCommand(program) {
99
99
  .description("Share file paths. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
100
100
  .option("--with <principal>", "Email address, group id, or '@all' to share with every active company member")
101
101
  .option("--permission <level>", "Permission level (only with --with): read | write")
102
+ .option("--full", "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.")
102
103
  .option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
103
104
  .option("--no-open", "Print the share-session URL but do not launch the browser")
104
105
  .action(async (paths, opts) => {
105
106
  try {
107
+ // Full-vault grant: a glob-safe affordance for "give this person the
108
+ // whole vault" so admins never have to quote a `*` (an unquoted glob
109
+ // expands to local filenames and instantly fails the one-prefix
110
+ // check). Maps to the single `*` wildcard grant, which the server
111
+ // coalesces to one policy entry — sidestepping the per-prefix STS
112
+ // session-policy budget. Defaults to write permission.
113
+ if (opts.full) {
114
+ if (opts.with === undefined) {
115
+ console.error(chalk.red("--full grants whole-vault access to a principal and requires --with <principal>."));
116
+ process.exit(1);
117
+ }
118
+ if (paths && paths.length > 0) {
119
+ console.error(chalk.red("--full grants the entire vault; do not also pass file paths."));
120
+ process.exit(1);
121
+ }
122
+ await runDirectGrant({
123
+ prefix: "*",
124
+ principal: opts.with,
125
+ permission: opts.permission ?? "write",
126
+ companySlug: files.opts().company,
127
+ fullVault: true,
128
+ });
129
+ return;
130
+ }
106
131
  if (!paths || paths.length === 0) {
107
132
  console.error(chalk.red("usage: hq files share <paths...> [--with <principal>]"));
108
133
  process.exit(1);
@@ -437,7 +462,12 @@ async function runDirectGrant(params) {
437
462
  const data = (await res.json());
438
463
  const printedPrefix = data.acl?.path ?? data.acl?.prefix ?? canonicalPrefix;
439
464
  const verb = autoCreated ? "Created ACL and granted" : "Granted";
440
- console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
465
+ if (params.fullVault) {
466
+ console.log(chalk.green(`${verb} ${params.permission} on the ENTIRE vault to ${principalLabel}`));
467
+ }
468
+ else {
469
+ console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
470
+ }
441
471
  }
442
472
  async function runShareSession(params) {
443
473
  // Normalize every path through the shared prefix helper so a trailing
@@ -682,4 +712,4 @@ export async function runFilesDelete(params, deps = {}) {
682
712
  }
683
713
  }
684
714
  //# sourceMappingURL=files.js.map
685
- //# debugId=bc564dd4-d8c1-5d09-ad49-3bcd9f7328a6
715
+ //# debugId=fc999359-c426-5277-ba96-5452ae1ed2e2
@@ -12,6 +12,22 @@ export interface PendingInvite {
12
12
  invitedBy: string;
13
13
  invitedAt: string;
14
14
  }
15
+ /**
16
+ * An ACTIVE member of a company, as returned by
17
+ * `GET /membership/company/{companyUid}`. The server filters to
18
+ * `status: "active"` and enriches each row with resolved person metadata
19
+ * (`personEmail` / `personName` / `personSlug`) when available.
20
+ */
21
+ export interface ActiveMember {
22
+ membershipKey: string;
23
+ personUid: string;
24
+ companyUid: string;
25
+ role: string;
26
+ status: string;
27
+ personEmail?: string;
28
+ personName?: string;
29
+ personSlug?: string;
30
+ }
15
31
  export interface InviteOptions {
16
32
  target: string;
17
33
  role: string;
@@ -114,6 +130,7 @@ export declare class InviteHttpError extends Error {
114
130
  }
115
131
  export declare function formatInviteHttpError(status: number, fallback: string, code?: string): string;
116
132
  export declare function listPendingInvites(token: string, companyUid: string): Promise<PendingInvite[]>;
133
+ export declare function listActiveMembers(token: string, companyUid: string): Promise<ActiveMember[]>;
117
134
  /**
118
135
  * Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
119
136
  * the server requires. Accepts three input forms:
@@ -132,5 +149,15 @@ export declare function listPendingInvites(token: string, companyUid: string): P
132
149
  */
133
150
  export declare function resolveRevokeTargetToMembershipKey(arg: string, companyUid: string): string;
134
151
  export declare function revokeInvite(token: string, tokenOrKey: string, companyUid: string): Promise<void>;
152
+ /**
153
+ * Change a member's role via `POST /membership/role`, accepting the FULL role
154
+ * set (owner|admin|member|guest). This is a GENERAL role change — it can promote
155
+ * OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
156
+ * promote-to-owner / change-an-owner) is enforced SERVER-side; this function
157
+ * only validates the role string locally and surfaces the server's error.
158
+ * Role string is validated BEFORE any network call so callers/tests can rely on
159
+ * a synchronous-shaped rejection for a bad role.
160
+ */
161
+ export declare function changeMemberRole(token: string, companyUid: string, membershipKey: string, newRole: Role): Promise<void>;
135
162
  export declare function registerMembersCommand(program: Command): void;
136
163
  //# sourceMappingURL=members.d.ts.map
@@ -1,12 +1,11 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="40f5379c-742e-5cd5-aba2-931856dc7dd3")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="83a54ed2-3f52-5b6f-a784-09f78f488ddd")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import { ensureCognitoToken } from "../utils/cognito-session.js";
5
5
  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
  export function detectTarget(target) {
11
10
  if (EMAIL_PATTERN.test(target)) {
12
11
  return { type: "email", value: target.trim().toLowerCase() };
@@ -196,6 +195,20 @@ export async function listPendingInvites(token, companyUid) {
196
195
  const data = (await res.json());
197
196
  return data?.pending ?? data?.invites ?? [];
198
197
  }
198
+ export async function listActiveMembers(token, companyUid) {
199
+ const res = await vaultApiFetch({
200
+ token,
201
+ path: `/membership/company/${encodeURIComponent(companyUid)}`,
202
+ });
203
+ if (!res.ok) {
204
+ const err = (await res.json().catch(() => ({})));
205
+ throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
206
+ }
207
+ // Server schema: `{ members: [...] }` — active members only, enriched with
208
+ // resolved person metadata (personEmail / personName / personSlug).
209
+ const data = (await res.json());
210
+ return data?.members ?? [];
211
+ }
199
212
  /**
200
213
  * Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
201
214
  * the server requires. Accepts three input forms:
@@ -251,9 +264,18 @@ export async function revokeInvite(token, tokenOrKey, companyUid) {
251
264
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
252
265
  }
253
266
  }
254
- async function setMemberRole(token, companyUid, membershipKey, newRole) {
255
- if (!VALID_MEMBER_SET_ROLES.has(newRole)) {
256
- throw new Error(`Invalid role '${newRole}': must be one of admin, member`);
267
+ /**
268
+ * Change a member's role via `POST /membership/role`, accepting the FULL role
269
+ * set (owner|admin|member|guest). This is a GENERAL role change it can promote
270
+ * OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
271
+ * promote-to-owner / change-an-owner) is enforced SERVER-side; this function
272
+ * only validates the role string locally and surfaces the server's error.
273
+ * Role string is validated BEFORE any network call so callers/tests can rely on
274
+ * a synchronous-shaped rejection for a bad role.
275
+ */
276
+ export async function changeMemberRole(token, companyUid, membershipKey, newRole) {
277
+ if (!VALID_ROLES.has(newRole)) {
278
+ throw new Error(`Invalid role '${newRole}': must be one of owner, admin, member, guest`);
257
279
  }
258
280
  const res = await vaultApiFetch({
259
281
  token,
@@ -271,16 +293,29 @@ export function registerMembersCommand(program) {
271
293
  .command("members")
272
294
  .description("Manage company memberships and invites")
273
295
  .option("--company <slug>", "Company slug (resolves to companyUid)");
296
+ // Canonical role-change command. `promote` is the discoverable verb users
297
+ // reach for; `set-role` is kept as an alias for the older name. The route is a
298
+ // GENERAL role change, so the help text and success message are honest that it
299
+ // can demote as well as promote. Authorization is enforced server-side.
274
300
  members
275
- .command("set-role <membershipKey> <role>")
276
- .description("Change a member or agent role to admin or member")
277
- .action(async (membershipKey, role) => {
301
+ .command("promote <target> <newRole>")
302
+ .alias("set-role")
303
+ .description("Change a member's role (owner|admin|member|guest) promotes OR demotes. " +
304
+ "<target> may be an email, a prs_ personUid, or a full membership key. " +
305
+ "Owner-or-admin only; setting a target to owner (or changing an owner) is owner-only. Server-enforced.")
306
+ .action(async (target, newRole) => {
278
307
  try {
308
+ const role = newRole.trim().toLowerCase();
309
+ if (!VALID_ROLES.has(role)) {
310
+ console.error(chalk.red(`Invalid role '${newRole}': must be one of owner, admin, member, guest`));
311
+ process.exit(1);
312
+ }
279
313
  const token = await ensureCognitoToken();
280
314
  const companySlug = members.opts().company;
281
315
  const companyUid = await getCompanyUid(token, companySlug);
282
- await setMemberRole(token, companyUid, membershipKey, role);
283
- console.log(chalk.green(`Updated role for '${membershipKey}' to ${role}`));
316
+ const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
317
+ await changeMemberRole(token, companyUid, membershipKey, role);
318
+ console.log(chalk.green(`Updated role for '${target}' to ${role}`));
284
319
  }
285
320
  catch (err) {
286
321
  if (err instanceof InviteHttpError) {
@@ -422,43 +457,66 @@ export function registerMembersCommand(program) {
422
457
  });
423
458
  members
424
459
  .command("list")
425
- .description("List pending invites for the company")
426
- .action(async () => {
460
+ .description("List the company's active members (use --pending for pending invites)")
461
+ .option("--pending", "List pending invites instead of active members")
462
+ .action(async (opts) => {
427
463
  try {
428
464
  const token = await ensureCognitoToken();
429
465
  const companySlug = members.opts().company;
430
466
  const companyUid = await getCompanyUid(token, companySlug);
431
- const invites = await listPendingInvites(token, companyUid);
432
- if (invites.length === 0) {
433
- console.log(chalk.gray("No pending invites for this company."));
467
+ if (opts.pending) {
468
+ // --pending: preserve the original pending-invites view verbatim.
469
+ const invites = await listPendingInvites(token, companyUid);
470
+ if (invites.length === 0) {
471
+ console.log(chalk.gray("No pending invites for this company."));
472
+ return;
473
+ }
474
+ const targetW = Math.max(6, ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length));
475
+ const roleW = Math.max(4, ...invites.map((i) => i.role.length));
476
+ const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
477
+ const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
478
+ console.log(chalk.bold([
479
+ "TARGET".padEnd(targetW),
480
+ "ROLE".padEnd(roleW),
481
+ "INVITED_BY".padEnd(byW),
482
+ "INVITED_AT",
483
+ "MEMBERSHIP_KEY".padEnd(keyW),
484
+ ].join(" ")));
485
+ for (const inv of invites) {
486
+ const target = inv.inviteeEmail ?? inv.personUid ?? "";
487
+ console.log([
488
+ target.padEnd(targetW),
489
+ inv.role.padEnd(roleW),
490
+ inv.invitedBy.padEnd(byW),
491
+ shortDate(inv.invitedAt),
492
+ inv.membershipKey.padEnd(keyW),
493
+ ].join(" "));
494
+ }
495
+ return;
496
+ }
497
+ // Default: list ACTIVE members so their emails drop straight into
498
+ // `hq secrets share <path> --with <email>`.
499
+ const activeMembers = await listActiveMembers(token, companyUid);
500
+ if (activeMembers.length === 0) {
501
+ console.log(chalk.gray("No active members found for this company."));
434
502
  return;
435
503
  }
436
- const targetW = Math.max(6, ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length));
437
- const roleW = Math.max(4, ...invites.map((i) => i.role.length));
438
- const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
439
- const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
440
- console.log(chalk.bold([
441
- "TARGET".padEnd(targetW),
442
- "ROLE".padEnd(roleW),
443
- "INVITED_BY".padEnd(byW),
444
- "INVITED_AT",
445
- "MEMBERSHIP_KEY".padEnd(keyW),
446
- ].join(" ")));
447
- for (const inv of invites) {
448
- const target = inv.inviteeEmail ?? inv.personUid ?? "";
449
- console.log([
450
- target.padEnd(targetW),
451
- inv.role.padEnd(roleW),
452
- inv.invitedBy.padEnd(byW),
453
- shortDate(inv.invitedAt),
454
- inv.membershipKey.padEnd(keyW),
455
- ].join(" "));
504
+ const emailW = Math.max(5, ...activeMembers.map((m) => (m.personEmail ?? m.personUid).length));
505
+ const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
506
+ console.log(chalk.bold(["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW), "NAME"].join(" ")));
507
+ for (const m of activeMembers) {
508
+ const email = m.personEmail ?? m.personUid;
509
+ const name = m.personName ?? m.personSlug ?? "";
510
+ console.log([email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "));
456
511
  }
512
+ console.log(chalk.gray("Share a secret with a member: hq secrets share <path> --with <email>"));
457
513
  }
458
514
  catch (err) {
459
515
  if (err instanceof InviteHttpError) {
460
516
  const msg = err.status === 403
461
- ? "Not authorized — only admins and owners can list invites"
517
+ ? opts.pending
518
+ ? "Not authorized — only admins and owners can list invites"
519
+ : "Not authorized — only company members can list members"
462
520
  : formatInviteHttpError(err.status, err.message);
463
521
  console.error(chalk.red(msg));
464
522
  process.exit(1);
@@ -497,4 +555,4 @@ export function registerMembersCommand(program) {
497
555
  });
498
556
  }
499
557
  //# sourceMappingURL=members.js.map
500
- //# debugId=40f5379c-742e-5cd5-aba2-931856dc7dd3
558
+ //# debugId=83a54ed2-3f52-5b6f-a784-09f78f488ddd
@@ -11,6 +11,16 @@
11
11
  * named by `--company`); nothing reads across company boundaries.
12
12
  */
13
13
  import { Command } from "commander";
14
+ import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
15
+ export type RefreshPeopleRoster = (hqRoot: string, companySlug: string) => Promise<void>;
16
+ interface PeopleCommandDeps {
17
+ refreshRoster?: RefreshPeopleRoster;
18
+ }
19
+ interface PeopleLookupOpts {
20
+ localOnly?: boolean;
21
+ json?: boolean;
22
+ }
23
+ export declare function refreshPeopleRosterFromCloud(hqRoot: string, companySlug: string): Promise<void>;
14
24
  /**
15
25
  * Resolve the single company to operate on. Explicit `--company` always wins
16
26
  * (after a path-safety check). Otherwise the active company is inferred from
@@ -18,5 +28,20 @@ import { Command } from "commander";
18
28
  * do, the caller must disambiguate with `--company`.
19
29
  */
20
30
  export declare function resolveCompanySlug(hqRoot: string, explicit: string | undefined): string;
21
- export declare function registerPeopleCommand(program: Command): void;
31
+ export declare function resolvePersonWithRosterFallback(input: {
32
+ hqRoot: string;
33
+ slug: string;
34
+ name: string;
35
+ opts?: PeopleLookupOpts;
36
+ refreshRoster?: RefreshPeopleRoster;
37
+ }): Promise<ReturnType<typeof resolveNameToEmail>>;
38
+ export declare function searchPeopleWithRosterFallback(input: {
39
+ hqRoot: string;
40
+ slug: string;
41
+ keyword: string;
42
+ opts?: PeopleLookupOpts;
43
+ refreshRoster?: RefreshPeopleRoster;
44
+ }): Promise<PersonRecord[]>;
45
+ export declare function registerPeopleCommand(program: Command, deps?: PeopleCommandDeps): void;
46
+ export {};
22
47
  //# sourceMappingURL=people.d.ts.map
@@ -11,14 +11,31 @@
11
11
  * named by `--company`); nothing reads across company boundaries.
12
12
  */
13
13
 
14
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="076b7f64-6626-5733-8992-ab9361a3e31d")}catch(e){}}();
14
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="79abc1f5-4f5a-5c95-b29b-10f04417355f")}catch(e){}}();
15
15
  import * as fs from "fs";
16
16
  import { Option } from "commander";
17
17
  import chalk from "chalk";
18
+ import { VaultClient } from "@indigoai-us/hq-cloud";
18
19
  import * as yaml from "js-yaml";
19
20
  import { findHqRoot } from "../utils/manifest.js";
20
21
  import { manifestPath } from "./cloud-provision.js";
22
+ import { DEFAULT_COGNITO, buildVaultConfig, ensureCognitoToken, } from "../utils/cognito-session.js";
23
+ import { getCompanyUid } from "../utils/vault-api.js";
24
+ import { createCompanyPresignClient, runGet } from "./files-browse.js";
21
25
  import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
26
+ export async function refreshPeopleRosterFromCloud(hqRoot, companySlug) {
27
+ const accessToken = await ensureCognitoToken();
28
+ const client = new VaultClient(buildVaultConfig(accessToken));
29
+ await getCompanyUid(accessToken, companySlug);
30
+ await runGet({
31
+ path: `companies/${companySlug}/people/`,
32
+ hqRoot,
33
+ companySlug,
34
+ vaultClient: client,
35
+ companyClient: ({ companyUid }) => createCompanyPresignClient({ token: accessToken, companyUid }),
36
+ region: DEFAULT_COGNITO.region,
37
+ });
38
+ }
22
39
  /** Companies that still exist (anything not explicitly `status: archived`). */
23
40
  function activeCompanySlugs(manifest) {
24
41
  const companies = manifest.companies ?? {};
@@ -86,7 +103,39 @@ function fail(message) {
86
103
  console.error(chalk.red(message));
87
104
  process.exit(1);
88
105
  }
89
- export function registerPeopleCommand(program) {
106
+ function logRefreshFailure(companySlug, err) {
107
+ const message = err instanceof Error ? err.message : String(err);
108
+ console.error(chalk.dim(` Could not refresh people roster for '${companySlug}': ${message}`));
109
+ }
110
+ async function tryRefreshRoster(refreshRoster, hqRoot, slug) {
111
+ try {
112
+ await refreshRoster(hqRoot, slug);
113
+ return true;
114
+ }
115
+ catch (err) {
116
+ logRefreshFailure(slug, err);
117
+ return false;
118
+ }
119
+ }
120
+ export async function resolvePersonWithRosterFallback(input) {
121
+ const local = resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
122
+ if (local.status !== "not_found" || input.opts?.localOnly)
123
+ return local;
124
+ const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
125
+ if (!refreshed)
126
+ return local;
127
+ return resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
128
+ }
129
+ export async function searchPeopleWithRosterFallback(input) {
130
+ const local = searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
131
+ if (local.length > 0 || input.opts?.localOnly)
132
+ return local;
133
+ const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
134
+ if (!refreshed)
135
+ return local;
136
+ return searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
137
+ }
138
+ export function registerPeopleCommand(program, deps = {}) {
90
139
  const people = program
91
140
  .command("people")
92
141
  .description("List, search, and resolve a company's people (from companies/<co>/people)")
@@ -122,12 +171,19 @@ export function registerPeopleCommand(program) {
122
171
  .command("search <keyword>")
123
172
  .description("Keyword search over people names and emails")
124
173
  .option("--json", "Output JSON instead of a table")
125
- .action((keyword, opts) => {
174
+ .option("--local-only", "Skip cloud fallback; search only the local people roster")
175
+ .action(async (keyword, opts) => {
126
176
  try {
127
177
  const scope = people.opts();
128
178
  const hqRoot = resolveHqRoot(scope);
129
179
  const slug = resolveCompanySlug(hqRoot, scope.company);
130
- const matches = searchPeople(listCompanyPeople(hqRoot, slug), keyword);
180
+ const matches = await searchPeopleWithRosterFallback({
181
+ hqRoot,
182
+ slug,
183
+ keyword,
184
+ opts,
185
+ refreshRoster: deps.refreshRoster,
186
+ });
131
187
  if (opts.json) {
132
188
  console.log(JSON.stringify(matches, null, 2));
133
189
  return;
@@ -146,12 +202,19 @@ export function registerPeopleCommand(program) {
146
202
  .command("resolve <name>")
147
203
  .description("Resolve a person name to their email address")
148
204
  .option("--json", "Output JSON instead of plain text")
149
- .action((name, opts) => {
205
+ .option("--local-only", "Skip cloud fallback; resolve only from the local people roster")
206
+ .action(async (name, opts) => {
150
207
  try {
151
208
  const scope = people.opts();
152
209
  const hqRoot = resolveHqRoot(scope);
153
210
  const slug = resolveCompanySlug(hqRoot, scope.company);
154
- const result = resolveNameToEmail(listCompanyPeople(hqRoot, slug), name);
211
+ const result = await resolvePersonWithRosterFallback({
212
+ hqRoot,
213
+ slug,
214
+ name,
215
+ opts,
216
+ refreshRoster: deps.refreshRoster,
217
+ });
155
218
  if (opts.json) {
156
219
  console.log(JSON.stringify(result, null, 2));
157
220
  if (result.status === "found")
@@ -184,4 +247,4 @@ export function registerPeopleCommand(program) {
184
247
  });
185
248
  }
186
249
  //# sourceMappingURL=people.js.map
187
- //# debugId=076b7f64-6626-5733-8992-ab9361a3e31d
250
+ //# debugId=79abc1f5-4f5a-5c95-b29b-10f04417355f
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure helpers describing WHICH secrets scope a command acted on, so `set` and
3
+ * `list` can echo it. Users were setting a secret in one scope (personal vs a
4
+ * company, or company A vs B) and listing another, then seeing "no secrets" with
5
+ * no indication the scopes differed (feedback_70e059da).
6
+ */
7
+ export interface SecretsScopeRef {
8
+ /** True when --personal was used (caller's personal vault). */
9
+ personal: boolean;
10
+ /** The slug the user passed via --company, if any. */
11
+ companySlug?: string;
12
+ /** Resolved entity uid: prs_* for personal, cmp_* for a company. */
13
+ companyUid: string;
14
+ }
15
+ /** Human label for a secrets scope: "your personal vault" or "company <slug-or-uid>". */
16
+ export declare function describeSecretsScope(ref: SecretsScopeRef): string;
17
+ export declare function formatSecretSaved(name: string, scope: string): string;
18
+ export declare function formatSecretsListHeader(scope: string): string;
19
+ export declare function formatSecretsListEmpty(scope: string): string;
20
+ //# sourceMappingURL=secrets-scope.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Human label for a secrets scope: "your personal vault" or "company <slug-or-uid>". */
2
+
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="718913a3-f80f-5bff-928e-a7aa9f51dd0d")}catch(e){}}();
4
+ export function describeSecretsScope(ref) {
5
+ if (ref.personal)
6
+ return "your personal vault";
7
+ return `company ${ref.companySlug ?? ref.companyUid}`;
8
+ }
9
+ export function formatSecretSaved(name, scope) {
10
+ return `Secret '${name}' saved to ${scope}.`;
11
+ }
12
+ export function formatSecretsListHeader(scope) {
13
+ return `Secrets for ${scope}:`;
14
+ }
15
+ export function formatSecretsListEmpty(scope) {
16
+ return `No secrets found for ${scope}.`;
17
+ }
18
+ //# sourceMappingURL=secrets-scope.js.map
19
+ //# debugId=718913a3-f80f-5bff-928e-a7aa9f51dd0d
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3049e431-c5db-5430-90fe-82866c35e95b")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="96461f1a-6d5b-5dc0-b00a-0a9fa30a0dad")}catch(e){}}();
3
3
  import chalk from "chalk";
4
4
  import * as readline from "node:readline";
5
5
  import { spawn } from "node:child_process";
@@ -8,6 +8,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
8
8
  import { DEFAULT_SECRETS_CACHE_TTL_MS, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
9
9
  import { computeSha256 } from "../utils/integrity.js";
10
10
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
11
+ import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
11
12
  import { vaultApiFetch, getCompanyUid, getEntityUid, } from "../utils/vault-api.js";
12
13
  export { vaultApiFetch, getCompanyUid, getEntityUid };
13
14
  function scopeOpts(opts) {
@@ -344,7 +345,13 @@ export function registerSecretsCommand(program) {
344
345
  process.exit(1);
345
346
  }
346
347
  const token = await ensureCognitoToken();
347
- const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
348
+ const scope = scopeOpts(secrets.opts());
349
+ const companyUid = await getEntityUid(token, scope);
350
+ const scopeLabel = describeSecretsScope({
351
+ personal: scope.personal,
352
+ companySlug: scope.companySlug,
353
+ companyUid,
354
+ });
348
355
  const res = await vaultApiFetch({
349
356
  token,
350
357
  path: `/secrets/${encodeURIComponent(companyUid)}`,
@@ -357,7 +364,7 @@ export function registerSecretsCommand(program) {
357
364
  process.exit(1);
358
365
  }
359
366
  removeCacheEntry(companyUid, name);
360
- console.log(chalk.green(`Secret '${name}' saved.`));
367
+ console.log(chalk.green(formatSecretSaved(name, scopeLabel)));
361
368
  }
362
369
  catch (err) {
363
370
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -487,7 +494,13 @@ export function registerSecretsCommand(program) {
487
494
  normalizedPrefix = normalized;
488
495
  }
489
496
  const token = await ensureCognitoToken();
490
- const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
497
+ const scope = scopeOpts(secrets.opts());
498
+ const companyUid = await getEntityUid(token, scope);
499
+ const scopeLabel = describeSecretsScope({
500
+ personal: scope.personal,
501
+ companySlug: scope.companySlug,
502
+ companyUid,
503
+ });
491
504
  const query = {};
492
505
  if (normalizedPrefix) {
493
506
  query.prefix = normalizedPrefix;
@@ -504,7 +517,7 @@ export function registerSecretsCommand(program) {
504
517
  }
505
518
  const data = (await res.json());
506
519
  if (data.secrets.length === 0) {
507
- console.log(chalk.dim("No secrets found."));
520
+ console.log(chalk.dim(formatSecretsListEmpty(scopeLabel)));
508
521
  return;
509
522
  }
510
523
  const nameWidth = Math.max(4, ...data.secrets.map((s) => s.name.length));
@@ -514,6 +527,7 @@ export function registerSecretsCommand(program) {
514
527
  if (hasPermission) {
515
528
  const accessWidth = Math.max(6, ...data.secrets.map((s) => (s.permission ?? "-").length));
516
529
  const header = `${"NAME".padEnd(nameWidth)} ${"ACCESS".padEnd(accessWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
530
+ console.log(chalk.dim(formatSecretsListHeader(scopeLabel)));
517
531
  console.log(chalk.bold(header));
518
532
  for (const s of data.secrets) {
519
533
  const access = s.permission ?? "-";
@@ -525,6 +539,7 @@ export function registerSecretsCommand(program) {
525
539
  }
526
540
  else {
527
541
  const header = `${"NAME".padEnd(nameWidth)} ${"TIER".padEnd(tierWidth)} ${"SCRIPT LOCK".padEnd(scriptLockWidth)} LAST MODIFIED`;
542
+ console.log(chalk.dim(formatSecretsListHeader(scopeLabel)));
528
543
  console.log(chalk.bold(header));
529
544
  for (const s of data.secrets) {
530
545
  const tier = normalizeSecretTier(s.tier);
@@ -1148,4 +1163,4 @@ export function registerSecretsCommand(program) {
1148
1163
  });
1149
1164
  }
1150
1165
  //# sourceMappingURL=secrets.js.map
1151
- //# debugId=3049e431-c5db-5430-90fe-82866c35e95b
1166
+ //# debugId=96461f1a-6d5b-5dc0-b00a-0a9fa30a0dad