@indigoai-us/hq-cli 5.50.2 → 5.51.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 (39) hide show
  1. package/dist/bin/hq-auth-refresh.d.ts +1 -1
  2. package/dist/bin/hq-auth-refresh.js +5 -2
  3. package/dist/commands/members.d.ts +17 -0
  4. package/dist/commands/members.js +65 -28
  5. package/dist/commands/people.d.ts +26 -1
  6. package/dist/commands/people.js +70 -7
  7. package/dist/commands/secrets-scope.d.ts +20 -0
  8. package/dist/commands/secrets-scope.js +19 -0
  9. package/dist/commands/secrets.js +21 -6
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +44 -14
  12. package/dist/node-preflight.d.ts +39 -0
  13. package/dist/node-preflight.js +55 -0
  14. package/dist/sentry.d.ts +12 -0
  15. package/dist/sentry.js +19 -3
  16. package/dist/utils/epipe.d.ts +8 -0
  17. package/dist/utils/epipe.js +30 -0
  18. package/dist/utils/intercepted-process-exit.d.ts +7 -0
  19. package/dist/utils/intercepted-process-exit.js +38 -0
  20. package/e2e/cli.test.ts +35 -0
  21. package/package.json +1 -1
  22. package/src/bin/hq-auth-refresh.ts +3 -0
  23. package/src/commands/members.test.ts +176 -0
  24. package/src/commands/members.ts +113 -28
  25. package/src/commands/people.test.ts +212 -5
  26. package/src/commands/people.ts +141 -5
  27. package/src/commands/secrets-scope.test.ts +56 -0
  28. package/src/commands/secrets-scope.ts +32 -0
  29. package/src/commands/secrets.ts +24 -10
  30. package/src/index.ts +40 -12
  31. package/src/node-preflight.test.ts +60 -0
  32. package/src/node-preflight.ts +67 -0
  33. package/src/sentry-epipe.test.ts +37 -0
  34. package/src/sentry-release.test.ts +54 -0
  35. package/src/sentry.ts +21 -1
  36. package/src/utils/epipe.test.ts +28 -0
  37. package/src/utils/epipe.ts +29 -0
  38. package/src/utils/intercepted-process-exit.test.ts +37 -0
  39. package/src/utils/intercepted-process-exit.ts +36 -0
@@ -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
@@ -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:
@@ -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]="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]="8244d720-094e-5d4e-b813-75086ed0849d")}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";
@@ -196,6 +196,20 @@ export async function listPendingInvites(token, companyUid) {
196
196
  const data = (await res.json());
197
197
  return data?.pending ?? data?.invites ?? [];
198
198
  }
199
+ export async function listActiveMembers(token, companyUid) {
200
+ const res = await vaultApiFetch({
201
+ token,
202
+ path: `/membership/company/${encodeURIComponent(companyUid)}`,
203
+ });
204
+ if (!res.ok) {
205
+ const err = (await res.json().catch(() => ({})));
206
+ throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
207
+ }
208
+ // Server schema: `{ members: [...] }` — active members only, enriched with
209
+ // resolved person metadata (personEmail / personName / personSlug).
210
+ const data = (await res.json());
211
+ return data?.members ?? [];
212
+ }
199
213
  /**
200
214
  * Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
201
215
  * the server requires. Accepts three input forms:
@@ -422,43 +436,66 @@ export function registerMembersCommand(program) {
422
436
  });
423
437
  members
424
438
  .command("list")
425
- .description("List pending invites for the company")
426
- .action(async () => {
439
+ .description("List the company's active members (use --pending for pending invites)")
440
+ .option("--pending", "List pending invites instead of active members")
441
+ .action(async (opts) => {
427
442
  try {
428
443
  const token = await ensureCognitoToken();
429
444
  const companySlug = members.opts().company;
430
445
  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."));
446
+ if (opts.pending) {
447
+ // --pending: preserve the original pending-invites view verbatim.
448
+ const invites = await listPendingInvites(token, companyUid);
449
+ if (invites.length === 0) {
450
+ console.log(chalk.gray("No pending invites for this company."));
451
+ return;
452
+ }
453
+ const targetW = Math.max(6, ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length));
454
+ const roleW = Math.max(4, ...invites.map((i) => i.role.length));
455
+ const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
456
+ const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
457
+ console.log(chalk.bold([
458
+ "TARGET".padEnd(targetW),
459
+ "ROLE".padEnd(roleW),
460
+ "INVITED_BY".padEnd(byW),
461
+ "INVITED_AT",
462
+ "MEMBERSHIP_KEY".padEnd(keyW),
463
+ ].join(" ")));
464
+ for (const inv of invites) {
465
+ const target = inv.inviteeEmail ?? inv.personUid ?? "";
466
+ console.log([
467
+ target.padEnd(targetW),
468
+ inv.role.padEnd(roleW),
469
+ inv.invitedBy.padEnd(byW),
470
+ shortDate(inv.invitedAt),
471
+ inv.membershipKey.padEnd(keyW),
472
+ ].join(" "));
473
+ }
474
+ return;
475
+ }
476
+ // Default: list ACTIVE members so their emails drop straight into
477
+ // `hq secrets share <path> --with <email>`.
478
+ const activeMembers = await listActiveMembers(token, companyUid);
479
+ if (activeMembers.length === 0) {
480
+ console.log(chalk.gray("No active members found for this company."));
434
481
  return;
435
482
  }
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(" "));
483
+ const emailW = Math.max(5, ...activeMembers.map((m) => (m.personEmail ?? m.personUid).length));
484
+ const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
485
+ console.log(chalk.bold(["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW), "NAME"].join(" ")));
486
+ for (const m of activeMembers) {
487
+ const email = m.personEmail ?? m.personUid;
488
+ const name = m.personName ?? m.personSlug ?? "";
489
+ console.log([email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "));
456
490
  }
491
+ console.log(chalk.gray("Share a secret with a member: hq secrets share <path> --with <email>"));
457
492
  }
458
493
  catch (err) {
459
494
  if (err instanceof InviteHttpError) {
460
495
  const msg = err.status === 403
461
- ? "Not authorized — only admins and owners can list invites"
496
+ ? opts.pending
497
+ ? "Not authorized — only admins and owners can list invites"
498
+ : "Not authorized — only company members can list members"
462
499
  : formatInviteHttpError(err.status, err.message);
463
500
  console.error(chalk.red(msg));
464
501
  process.exit(1);
@@ -497,4 +534,4 @@ export function registerMembersCommand(program) {
497
534
  });
498
535
  }
499
536
  //# sourceMappingURL=members.js.map
500
- //# debugId=40f5379c-742e-5cd5-aba2-931856dc7dd3
537
+ //# debugId=8244d720-094e-5d4e-b813-75086ed0849d
@@ -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
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@
2
2
  /**
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
- export {};
5
+ import "./node-preflight.js";
6
6
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,8 +2,11 @@
2
2
  /**
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
+ // MUST be first: guard the Node version before any dependency that needs a
6
+ // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
5
7
 
6
- !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]="34e9e978-4c0a-5113-82af-9a33e30686c6")}catch(e){}}();
8
+ !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]="82c44c14-a953-5644-9bf2-dd2c6d487d63")}catch(e){}}();
9
+ import "./node-preflight.js";
7
10
  import { Command } from "commander";
8
11
  import { initSentry, Sentry } from "./sentry.js";
9
12
  import { registerAddCommand } from "./commands/add.js";
@@ -47,12 +50,18 @@ import { registerRescueCommand } from "./commands/rescue.js";
47
50
  import { registerMcpCommand } from "./commands/mcp-status.js";
48
51
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
49
52
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
53
+ import { isEpipe } from "./utils/epipe.js";
54
+ import { isInterceptedProcessExit } from "./utils/intercepted-process-exit.js";
50
55
  import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
51
56
  import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
52
57
  import { CLI_VERSION } from "./cli-version.js";
53
- // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes the pipe early.
58
+ // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes
59
+ // the pipe early. This covers the ASYNC path — an 'error' event emitted on the
60
+ // stream. The SYNCHRONOUS path (a `write EPIPE` thrown straight out of
61
+ // console.log inside a command) is handled in the top-level catch below; both
62
+ // share `isEpipe` (HQ-6B).
54
63
  const onPipeError = (err) => {
55
- if (err.code === "EPIPE") {
64
+ if (isEpipe(err)) {
56
65
  process.exit(0);
57
66
  }
58
67
  throw err;
@@ -182,19 +191,40 @@ registerMcpCommand(program);
182
191
  await program.parseAsync();
183
192
  }
184
193
  catch (err) {
185
- // A full disk / exhausted quota / read-only filesystem is the user's
186
- // machine, not an HQ code defect. Surface a clear, actionable message and
187
- // skip Sentry capture so one full disk doesn't flood the tracker with
188
- // identical, unfixable crash reports (HQ-CLI-2). Genuine errors still go
189
- // to Sentry and still exit 1.
190
- const envMsg = environmentalFsErrorMessage(err);
191
- if (envMsg) {
192
- process.stderr.write(`hq: ${envMsg}\n`);
194
+ // A broken pipe (EPIPE) means the reader of `hq`'s output closed it early
195
+ // (`hq | head`, `source <(hq …)`, a parent that exited). That is normal
196
+ // Unix behavior with no user-facing degradation exit cleanly (0) and
197
+ // skip Sentry capture instead of shipping a fatal (HQ-6B). A synchronous
198
+ // `write EPIPE` thrown out of console.log lands here rather than on the
199
+ // stream 'error' listener above.
200
+ if (isEpipe(err)) {
201
+ process.exitCode = 0;
202
+ }
203
+ else if (isInterceptedProcessExit(err)) {
204
+ // A security/audit FUZZ harness replaced `process.exit` with a throw so it
205
+ // can keep exercising the binary. Commander calling `process.exit` for
206
+ // normal CLI control flow (e.g. an unknown command → exit 1) then surfaces
207
+ // here as that synthetic marker. It is a test-harness artifact, NOT an
208
+ // hq-cli defect — a real user's `process.exit` just exits, so nothing is
209
+ // thrown or captured. Skip Sentry capture (no signal, no user-facing
210
+ // degradation) and preserve the intended non-zero exit (HQ-CLI-3).
211
+ process.exitCode = 1;
193
212
  }
194
213
  else {
195
- Sentry.captureException(err);
214
+ // A full disk / exhausted quota / read-only filesystem is the user's
215
+ // machine, not an HQ code defect. Surface a clear, actionable message and
216
+ // skip Sentry capture so one full disk doesn't flood the tracker with
217
+ // identical, unfixable crash reports (HQ-CLI-2). Genuine errors still go
218
+ // to Sentry and still exit 1.
219
+ const envMsg = environmentalFsErrorMessage(err);
220
+ if (envMsg) {
221
+ process.stderr.write(`hq: ${envMsg}\n`);
222
+ }
223
+ else {
224
+ Sentry.captureException(err);
225
+ }
226
+ process.exitCode = 1;
196
227
  }
197
- process.exitCode = 1;
198
228
  }
199
229
  finally {
200
230
  // Release health: finalize the per-run session before the flush.
@@ -203,4 +233,4 @@ registerMcpCommand(program);
203
233
  }
204
234
  })();
205
235
  //# sourceMappingURL=index.js.map
206
- //# debugId=34e9e978-4c0a-5113-82af-9a33e30686c6
236
+ //# debugId=82c44c14-a953-5644-9bf2-dd2c6d487d63
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Runtime Node.js version guard for the hq CLI.
3
+ *
4
+ * HQ tooling requires Node.js 20 or newer. On older runtimes (notably Node 18)
5
+ * the CLI dies with cryptic failures long before reaching any of its own code:
6
+ * a native-module ABI mismatch from a prebuilt dependency, and a missing
7
+ * `util.styleText` (added in Node 20). Those errors give the user no hint that
8
+ * the real problem is just an old Node.
9
+ *
10
+ * This module exists to fail fast with an actionable message instead. It is
11
+ * imported FIRST by every CLI entry point (`index.ts`, `bin/hq-auth-refresh.ts`)
12
+ * so the check runs before commander, Sentry, or any dependency that needs a
13
+ * Node 20+ API or a newer native ABI is evaluated. ES modules evaluate their
14
+ * imports in source order, so as long as this is the first import in the entry
15
+ * module, the guard short-circuits an unsupported runtime cleanly.
16
+ *
17
+ * Keep this file dependency-free — it must not import anything that could itself
18
+ * fail to load on the very runtime it is trying to detect.
19
+ */
20
+ export declare const MIN_NODE_MAJOR = 20;
21
+ export interface NodeVersionCheck {
22
+ ok: boolean;
23
+ major: number;
24
+ message?: string;
25
+ }
26
+ /**
27
+ * Pure check: is the given Node version string (e.g. "18.19.0") supported?
28
+ * Defaults to the running runtime's version. An unparseable version is treated
29
+ * as supported so we never block a user on a version string we can't read.
30
+ */
31
+ export declare function checkNodeVersion(versionString?: string): NodeVersionCheck;
32
+ /**
33
+ * Side-effecting guard run on import: prints the upgrade message to stderr and
34
+ * exits 1 on an unsupported runtime. A no-op on Node 20+. Set
35
+ * `HQ_SKIP_NODE_PREFLIGHT=1` to bypass (used by the test runner, which already
36
+ * runs on a supported Node).
37
+ */
38
+ export declare function enforceNodeVersion(): void;
39
+ //# sourceMappingURL=node-preflight.d.ts.map