@indigoai-us/hq-cli 5.24.0 → 5.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,9 +80,29 @@ jobs:
80
80
  echo "published=true" >> "$GITHUB_OUTPUT"
81
81
  fi
82
82
 
83
+ # Sentry sourcemap upload runs on every tag push regardless of whether
84
+ # this run was the one that actually published to npm. The previous
85
+ # `if: steps.publish.outputs.published == 'true'` gate made re-runs
86
+ # useless when only the Sentry upload failed — the publish step's
87
+ # short-circuit (`npm view @ver already published`) zeroed out the
88
+ # output, so the upload step was skipped on every re-run. dist/ is
89
+ # rebuilt by the Build + Inject steps each run, so the upload is
90
+ # always operating on fresh, debug-id-stamped artifacts.
83
91
  - name: Upload sourcemaps to Sentry
84
- if: steps.publish.outputs.published == 'true'
85
92
  run: |
93
+ # Defang paste-time whitespace in SENTRY_AUTH_TOKEN. Sentry's
94
+ # auth-header parser returns HTTP 401 "Token string should not
95
+ # contain spaces" when the secret has a trailing \n or any
96
+ # embedded whitespace — observed on the v5.25.0 release after a
97
+ # copy-paste into the GH repo secret carried a literal \n. `tr -d`
98
+ # is the cheap structural guard so the workflow doesn't blow up
99
+ # on the next dirty paste.
100
+ SENTRY_AUTH_TOKEN=$(printf %s "$SENTRY_AUTH_TOKEN" | tr -d '[:space:]')
101
+ if [ -z "$SENTRY_AUTH_TOKEN" ]; then
102
+ echo "::error::SENTRY_AUTH_TOKEN secret is empty (or all whitespace) after sanitization — cannot upload sourcemaps"
103
+ exit 1
104
+ fi
105
+ export SENTRY_AUTH_TOKEN
86
106
  VER=$(jq -r .version package.json)
87
107
  npx -y @sentry/cli@^2 sourcemaps upload --release "hq-cli@$VER" dist/
88
108
  env:
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.25.0] — 2026-05-26
6
+
7
+ ### Added
8
+
9
+ - **`hq members invite --send-email` (default true).** Passes
10
+ `sendEmail: true` to hq-pro's `/membership/invite`, which renders
11
+ and sends the invite email via Resend server-side. Pre-resend hq-pro
12
+ versions ignore the field; the CLI falls back to the legacy
13
+ manual-notify printout. `--no-send-email` opts out for scripted
14
+ bulk invites. Consumes hq-pro PR #140.
15
+ - **`hq members invite --groups <ids>`.** Comma-separated secret-group
16
+ ids forwarded as `groupIds[]`; invitee is added atomically on first
17
+ sign-in. Server already supported the field as `pendingGroupIds`;
18
+ this surfaces it on the CLI. CLI-side guard rejects the personUid
19
+ + groupIds combo before the wire call.
20
+ - **`hq members invite <email> --resend`.** Short-circuits to hq-pro's
21
+ `resend: true` path (no DDB write, re-fires Resend email against the
22
+ existing pending row). New `resendInvite()` helper; email target only.
23
+ Surfaces 404 `INVITE_NOT_PENDING` via `InviteHttpError`.
24
+
25
+ ### Notes
26
+
27
+ - Version skips 5.21.x–5.24.x because the hq-cloud-bump release train
28
+ (5.21.0, 5.22.0, 5.23.0, 5.24.0) shipped while this PR was open and
29
+ consumed those slots. Original branch bumped to 5.21.0; rebased twice
30
+ and rebumped to 5.25.0 just before merge.
31
+
5
32
  ## [5.22.0] — 2026-05-25
6
33
 
7
34
  ### Changed
@@ -16,6 +16,17 @@ export interface InviteOptions {
16
16
  target: string;
17
17
  role: string;
18
18
  paths?: string;
19
+ /** Email-keyed invites only — secret-group ids to attach on claim. */
20
+ groupIds?: string[];
21
+ /**
22
+ * Opt the server-side Resend send in/out. When `true`, hq-pro renders +
23
+ * sends the invitation email server-side and returns `emailSent` /
24
+ * `emailSkipped` / `emailError`. When `false` or unset, the legacy
25
+ * no-email path runs and the CLI prints the manual sign-in instructions.
26
+ * Requires hq-pro that supports `sendEmail` (older servers ignore the
27
+ * field; CLI falls back to the legacy printout in that case).
28
+ */
29
+ sendEmail?: boolean;
19
30
  companyUid: string;
20
31
  callerUid: string;
21
32
  token: string;
@@ -33,6 +44,10 @@ export interface InviteOptions {
33
44
  * instructions instead.
34
45
  *
35
46
  * `membership` is always populated when the server returned 2xx.
47
+ *
48
+ * Email-related fields are populated only when hq-pro performed a
49
+ * server-side Resend send (caller passed `sendEmail: true` AND the
50
+ * server supports it). Pre-resend servers omit them entirely.
36
51
  */
37
52
  export interface InviteResult {
38
53
  inviteToken?: string;
@@ -44,6 +59,29 @@ export interface InviteResult {
44
59
  inviteToken?: string;
45
60
  inviteeEmail?: string;
46
61
  };
62
+ /** Resend send actually fired and accepted by Resend. */
63
+ emailSent?: boolean;
64
+ /** Resend skipped because the server has no RESEND_API_KEY configured. */
65
+ emailSkipped?: boolean;
66
+ /** Resend was attempted but failed — human-readable reason. */
67
+ emailError?: string;
68
+ }
69
+ export interface ResendInviteOptions {
70
+ inviteeEmail: string;
71
+ companyUid: string;
72
+ callerUid: string;
73
+ token: string;
74
+ }
75
+ /**
76
+ * Outcome of `hq members invite --resend`. No new membership row was
77
+ * created — the server's `resend: true` short-circuit just re-fires the
78
+ * Resend email against the existing pending row.
79
+ */
80
+ export interface ResendInviteResult {
81
+ resent: true;
82
+ emailSent: boolean;
83
+ emailSkipped: boolean;
84
+ emailError?: string;
47
85
  }
48
86
  export interface DetectedTarget {
49
87
  type: "email" | "person";
@@ -59,6 +97,16 @@ export declare function shortDate(iso: string): string;
59
97
  export declare function getCallerPersonUid(token: string): Promise<string>;
60
98
  /** Send a `/membership/invite` request and return the magic link. */
61
99
  export declare function inviteMember(options: InviteOptions): Promise<InviteResult>;
100
+ /**
101
+ * Re-fire the invite email against an existing pending row without
102
+ * creating a new membership row. Maps to hq-pro `/membership/invite`
103
+ * with `{ resend: true }` (see hq-pro PR #140). Server gates on the
104
+ * caller's admin/owner role.
105
+ *
106
+ * Returns the email-send status. 404 here means "no pending row exists
107
+ * for this email+company pair — re-invite without --resend first."
108
+ */
109
+ export declare function resendInvite(options: ResendInviteOptions): Promise<ResendInviteResult>;
62
110
  export declare class InviteHttpError extends Error {
63
111
  status: number;
64
112
  code?: string | undefined;
@@ -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]="c06b812a-ee4a-5701-a64b-cee2fcfef523")}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]="83c39bc9-e532-580d-bc3a-5f176eb5cba6")}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";
@@ -47,6 +47,9 @@ export async function inviteMember(options) {
47
47
  if (!detected) {
48
48
  throw new Error(`Invalid target '${options.target}': must be an email address or a personUid matching prs_<alphanumeric>`);
49
49
  }
50
+ if (options.groupIds && options.groupIds.length > 0 && detected.type === "person") {
51
+ throw new Error("--groups is only valid on email-keyed invites (server rejects personUid + groupIds with 400)");
52
+ }
50
53
  const allowedPrefixes = options.paths
51
54
  ? options.paths.split(",").map((p) => p.trim()).filter(Boolean)
52
55
  : undefined;
@@ -61,6 +64,11 @@ export async function inviteMember(options) {
61
64
  body.personUid = detected.value;
62
65
  if (allowedPrefixes)
63
66
  body.allowedPrefixes = allowedPrefixes;
67
+ if (options.groupIds && options.groupIds.length > 0) {
68
+ body.groupIds = options.groupIds;
69
+ }
70
+ if (options.sendEmail === true)
71
+ body.sendEmail = true;
64
72
  const res = await vaultApiFetch({
65
73
  token: options.token,
66
74
  path: "/membership/invite",
@@ -92,6 +100,58 @@ export async function inviteMember(options) {
92
100
  ? { inviteToken, magicLink: `hq://accept/${inviteToken}` }
93
101
  : {}),
94
102
  membership: data.membership,
103
+ // Pre-resend hq-pro versions omit these fields entirely — leave the
104
+ // result fields undefined so the caller's "is server-side email
105
+ // supported?" check works via `typeof result.emailSent === 'boolean'`.
106
+ ...(typeof data.emailSent === "boolean" ? { emailSent: data.emailSent } : {}),
107
+ ...(typeof data.emailSkipped === "boolean"
108
+ ? { emailSkipped: data.emailSkipped }
109
+ : {}),
110
+ ...(data.emailError ? { emailError: data.emailError } : {}),
111
+ };
112
+ }
113
+ /**
114
+ * Re-fire the invite email against an existing pending row without
115
+ * creating a new membership row. Maps to hq-pro `/membership/invite`
116
+ * with `{ resend: true }` (see hq-pro PR #140). Server gates on the
117
+ * caller's admin/owner role.
118
+ *
119
+ * Returns the email-send status. 404 here means "no pending row exists
120
+ * for this email+company pair — re-invite without --resend first."
121
+ */
122
+ export async function resendInvite(options) {
123
+ const detected = detectTarget(options.inviteeEmail);
124
+ if (!detected || detected.type !== "email") {
125
+ throw new Error("--resend requires an email target (the resend path is email-keyed-row only)");
126
+ }
127
+ const res = await vaultApiFetch({
128
+ token: options.token,
129
+ path: "/membership/invite",
130
+ method: "POST",
131
+ body: {
132
+ companyUid: options.companyUid,
133
+ // Server's invite handler still requires `role` even on the resend
134
+ // short-circuit. Pass the safe default — server ignores it on this
135
+ // path (the pending row already has its real role).
136
+ role: "member",
137
+ inviteeEmail: detected.value,
138
+ invitedBy: options.callerUid,
139
+ resend: true,
140
+ },
141
+ });
142
+ if (!res.ok) {
143
+ const err = (await res.json().catch(() => ({})));
144
+ throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
145
+ }
146
+ const data = (await res.json());
147
+ if (data.resent !== true) {
148
+ throw new Error("Resend endpoint returned 2xx without `resent: true` — the connected hq-pro likely doesn't support `resend: true` yet. Upgrade hq-pro or re-invite without --resend.");
149
+ }
150
+ return {
151
+ resent: true,
152
+ emailSent: data.emailSent === true,
153
+ emailSkipped: data.emailSkipped === true,
154
+ ...(data.emailError ? { emailError: data.emailError } : {}),
95
155
  };
96
156
  }
97
157
  export class InviteHttpError extends Error {
@@ -163,6 +223,21 @@ export function resolveRevokeTargetToMembershipKey(arg, companyUid) {
163
223
  }
164
224
  return arg;
165
225
  }
226
+ function printEmailStatus(opts) {
227
+ if (opts.sent) {
228
+ console.log(chalk.green(`✓ Invitation email sent to ${opts.recipient.toLowerCase()}`));
229
+ return;
230
+ }
231
+ if (opts.skipped) {
232
+ console.log(chalk.yellow(`⚠ Email send skipped — hq-pro has no RESEND_API_KEY configured for this stage.`));
233
+ return;
234
+ }
235
+ if (opts.error) {
236
+ console.log(chalk.yellow(`⚠ Email send failed: ${opts.error}`));
237
+ return;
238
+ }
239
+ console.log(chalk.yellow(`⚠ Email status unknown — hq-pro returned no email fields (likely a pre-resend deploy).`));
240
+ }
166
241
  export async function revokeInvite(token, tokenOrKey, companyUid) {
167
242
  const res = await vaultApiFetch({
168
243
  token,
@@ -182,19 +257,46 @@ export function registerMembersCommand(program) {
182
257
  .option("--company <slug>", "Company slug (resolves to companyUid)");
183
258
  members
184
259
  .command("invite <target>")
185
- .description("Invite a person to the company by email or personUid (prints a magic link)")
260
+ .description("Invite a person to the company by email or personUid (sends an invitation email by default)")
186
261
  .option("--role <role>", "Role for the invitee: owner, admin, member, or guest", "member")
187
262
  .option("--paths <prefixes>", "Comma-separated allowed prefixes (only valid with --role guest)")
263
+ .option("--groups <ids>", "Comma-separated secret-group ids the invitee will be added to on claim (email-keyed invites only)")
264
+ .option("--send-email", "Have hq-pro send an invitation email via Resend (default true for email-keyed invites). Pre-resend hq-pro versions ignore this and the legacy 'no email sent' instructions print instead.", true)
265
+ .option("--no-send-email", "Skip the server-side Resend send — only create the pending DDB row. Useful when you're scripting bulk invites and will send the announcement out-of-band.")
266
+ .option("--resend", "Re-fire the invitation email against an existing pending row without creating a new row. Maps to hq-pro `resend: true` short-circuit. Email-keyed invites only.")
188
267
  .action(async (target, opts) => {
189
268
  try {
190
269
  const token = await ensureCognitoToken();
191
270
  const companySlug = members.opts().company;
192
271
  const companyUid = await getCompanyUid(token, companySlug);
193
272
  const callerUid = await getCallerPersonUid(token);
273
+ // --resend short-circuits to the server's re-fire path. No new row.
274
+ if (opts.resend === true) {
275
+ const resendResult = await resendInvite({
276
+ inviteeEmail: target,
277
+ companyUid,
278
+ callerUid,
279
+ token,
280
+ });
281
+ console.log(chalk.green(`Re-fired invite email for ${target.toLowerCase()} (existing pending row left untouched)`));
282
+ console.log();
283
+ printEmailStatus({
284
+ recipient: target,
285
+ sent: resendResult.emailSent,
286
+ skipped: resendResult.emailSkipped,
287
+ error: resendResult.emailError,
288
+ });
289
+ return;
290
+ }
291
+ const groupIds = opts.groups
292
+ ? opts.groups.split(",").map((g) => g.trim()).filter(Boolean)
293
+ : undefined;
194
294
  const result = await inviteMember({
195
295
  target,
196
296
  role: opts.role,
197
297
  paths: opts.paths,
298
+ groupIds,
299
+ sendEmail: opts.sendEmail,
198
300
  companyUid,
199
301
  callerUid,
200
302
  token,
@@ -207,34 +309,60 @@ export function registerMembersCommand(program) {
207
309
  console.log(` ${result.magicLink}`);
208
310
  console.log();
209
311
  console.log(chalk.dim("Share this link with the invitee. They can run `hq onboard join --invite-token <token>` to accept."));
312
+ return;
210
313
  }
211
- else {
212
- // schemaVersion 2+ email-keyed authoritative membership row.
213
- // No magic link to share; invitee accepts by signing into HQ.
214
- //
215
- // CRITICAL UX NOTE: this CLI command does NOT send any email.
216
- // hq-pro only writes the DDB pending row; only the hq-console UI
217
- // path triggers Resend. Operators who run `hq members invite`
218
- // expecting an email to fly out get silently broken flows. The
219
- // output below uses chalk.yellow + an explicit "no email sent"
220
- // line so this never sneaks past again.
221
- const inviteeEmail = result.membership.inviteeEmail ??
222
- (typeof target === "string" && target.includes("@")
223
- ? target
224
- : undefined);
225
- console.log(chalk.yellow("⚠ No email was sent. `hq members invite` only creates the pending membership row."));
226
- console.log();
227
- console.log(chalk.bold("To complete the invite, do ONE of:"));
228
- console.log(` 1. Manually notify ${inviteeEmail ?? "the invitee"}: ask them to sign into HQ`);
229
- console.log(` at https://hq.getindigo.ai with that email address.`);
230
- console.log(` 2. Or use the hq-console UI at https://hq.getindigo.ai to issue the`);
231
- console.log(` invite instead — the UI path triggers an automated email via Resend.`);
232
- console.log();
233
- console.log(chalk.dim("The pending membership row claims itself on the invitee's first sign-in."));
314
+ // schemaVersion 2+ — email-keyed authoritative membership row.
315
+ // Three output branches depending on whether the server-side
316
+ // Resend send fired:
317
+ // (a) emailSent === true → "email sent" success line
318
+ // (b) emailSkipped === true server has no RESEND_API_KEY;
319
+ // fall back to the manual-notify instructions
320
+ // (c) emailSent/emailSkipped both undefined pre-resend
321
+ // hq-pro, doesn't understand `sendEmail` at all; fall
322
+ // back to the manual-notify instructions
323
+ const inviteeEmail = result.membership.inviteeEmail ??
324
+ (typeof target === "string" && target.includes("@")
325
+ ? target
326
+ : undefined);
327
+ const emailDelivered = result.emailSent === true;
328
+ if (emailDelivered) {
329
+ printEmailStatus({
330
+ recipient: inviteeEmail ?? target,
331
+ sent: true,
332
+ skipped: false,
333
+ error: undefined,
334
+ });
234
335
  if (result.membership.membershipKey) {
235
336
  console.log();
236
337
  console.log(chalk.dim(`Membership key: ${result.membership.membershipKey}`));
237
338
  }
339
+ return;
340
+ }
341
+ // Email-send failed or wasn't attempted — surface the manual path.
342
+ if (result.emailError) {
343
+ console.log(chalk.yellow(`⚠ Email send failed: ${result.emailError}`));
344
+ console.log();
345
+ }
346
+ console.log(chalk.yellow("⚠ No email was sent. The pending membership row exists; the invitee needs to sign in manually to claim it."));
347
+ console.log();
348
+ console.log(chalk.bold("To complete the invite, do ONE of:"));
349
+ console.log(` 1. Manually notify ${inviteeEmail ?? "the invitee"}: ask them to sign into HQ`);
350
+ console.log(` at https://hq.getindigo.ai with that email address.`);
351
+ console.log(` 2. Or use the hq-console UI at https://hq.getindigo.ai to issue the`);
352
+ console.log(` invite instead — the UI path triggers an automated email via Resend.`);
353
+ if (result.emailSkipped !== true && result.emailSent === undefined) {
354
+ console.log();
355
+ console.log(chalk.dim("Tip: this hq-pro deploy doesn't support server-side email yet (`sendEmail` ignored). Ask an operator to ship hq-pro PR #140 + provision the ResendApiKey SST secret."));
356
+ }
357
+ else if (result.emailSkipped === true) {
358
+ console.log();
359
+ console.log(chalk.dim("Tip: hq-pro reported `emailSkipped: true` — its `RESEND_API_KEY` SST secret isn't set for this stage."));
360
+ }
361
+ console.log();
362
+ console.log(chalk.dim("The pending membership row claims itself on the invitee's first sign-in."));
363
+ if (result.membership.membershipKey) {
364
+ console.log();
365
+ console.log(chalk.dim(`Membership key: ${result.membership.membershipKey}`));
238
366
  }
239
367
  }
240
368
  catch (err) {
@@ -323,4 +451,4 @@ export function registerMembersCommand(program) {
323
451
  });
324
452
  }
325
453
  //# sourceMappingURL=members.js.map
326
- //# debugId=c06b812a-ee4a-5701-a64b-cee2fcfef523
454
+ //# debugId=83c39bc9-e532-580d-bc3a-5f176eb5cba6
@@ -13,7 +13,7 @@
13
13
  * 9. Print next-step message
14
14
  */
15
15
 
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]="874fe8a4-2348-5f03-8600-c545cac13fc9")}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]="c6ab4668-f147-5612-bbfc-e1e80b2f018a")}catch(e){}}();
17
17
  import * as fs from 'fs';
18
18
  import * as os from 'os';
19
19
  import * as path from 'path';
@@ -21,7 +21,7 @@ import * as yaml from 'js-yaml';
21
21
  import { execSync } from 'child_process';
22
22
  import chalk from 'chalk';
23
23
  import { ensureCognitoToken } from '../utils/cognito-session.js';
24
- import { findHqRoot } from '../utils/hq-root.js';
24
+ import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
25
25
  import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
26
26
  import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
27
27
  import { addToRegistry } from '../utils/registry.js';
@@ -93,7 +93,7 @@ async function installPackage(slug, company) {
93
93
  }
94
94
  }
95
95
  // 7. Extract
96
- const hqRoot = findHqRoot();
96
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
97
97
  const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
98
98
  // Clean existing installation
99
99
  if (fs.existsSync(installDir)) {
@@ -156,4 +156,4 @@ async function installPackage(slug, company) {
156
156
  }
157
157
  }
158
158
  //# sourceMappingURL=pkg-install.js.map
159
- //# debugId=874fe8a4-2348-5f03-8600-c545cac13fc9
159
+ //# debugId=c6ab4668-f147-5612-bbfc-e1e80b2f018a
@@ -4,9 +4,9 @@
4
4
  * Graceful offline: if registry is unreachable, show cached data with a note.
5
5
  */
6
6
 
7
- !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]="ce4886ce-109a-5ace-b8d9-4e5bed61978a")}catch(e){}}();
7
+ !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]="2ca922cd-1115-5059-850d-3927226eb9ee")}catch(e){}}();
8
8
  import chalk from 'chalk';
9
- import { findHqRoot } from '../utils/hq-root.js';
9
+ import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
10
10
  import { readRegistry } from '../utils/registry.js';
11
11
  import { loadCachedTokens, isExpiring } from '@indigoai-us/hq-cloud';
12
12
  import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
@@ -26,7 +26,7 @@ export function registerPackageListCommand(parent) {
26
26
  });
27
27
  }
28
28
  async function listPackages() {
29
- const hqRoot = findHqRoot();
29
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
30
30
  const installed = readRegistry(hqRoot);
31
31
  // Print installed packages
32
32
  if (installed.length > 0) {
@@ -70,4 +70,4 @@ async function listPackages() {
70
70
  }
71
71
  }
72
72
  //# sourceMappingURL=pkg-list.js.map
73
- //# debugId=ce4886ce-109a-5ace-b8d9-4e5bed61978a
73
+ //# debugId=2ca922cd-1115-5059-850d-3927226eb9ee
@@ -6,11 +6,11 @@
6
6
  * 3. Print next-step message
7
7
  */
8
8
 
9
- !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]="b54529d6-de12-595e-b1c7-ab50b9f22eb5")}catch(e){}}();
9
+ !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]="e44da498-3e38-5643-b124-49aa2e91f42b")}catch(e){}}();
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import chalk from 'chalk';
13
- import { findHqRoot } from '../utils/hq-root.js';
13
+ import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
14
14
  import { removeFromRegistry, readRegistry } from '../utils/registry.js';
15
15
  export function registerPackageRemoveCommand(parent) {
16
16
  parent
@@ -27,7 +27,7 @@ export function registerPackageRemoveCommand(parent) {
27
27
  });
28
28
  }
29
29
  async function removePackage(slug) {
30
- const hqRoot = findHqRoot();
30
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
31
31
  const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
32
32
  // Verify it is actually installed
33
33
  const entries = readRegistry(hqRoot);
@@ -53,4 +53,4 @@ async function removePackage(slug) {
53
53
  console.log(chalk.cyan('Run /package-remove ' + slug + ' in Claude to clean up merged content.'));
54
54
  }
55
55
  //# sourceMappingURL=pkg-remove.js.map
56
- //# debugId=b54529d6-de12-595e-b1c7-ab50b9f22eb5
56
+ //# debugId=e44da498-3e38-5643-b124-49aa2e91f42b
@@ -5,7 +5,7 @@
5
5
  * If slug given: update only that package.
6
6
  */
7
7
 
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]="3ef2968d-eb65-594b-a506-4314b70dd3dc")}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]="35fae6a0-f8e1-5fde-90d8-34ae4adb3c1c")}catch(e){}}();
9
9
  import * as fs from 'fs';
10
10
  import * as os from 'os';
11
11
  import * as path from 'path';
@@ -13,7 +13,7 @@ import * as yaml from 'js-yaml';
13
13
  import { execSync } from 'child_process';
14
14
  import chalk from 'chalk';
15
15
  import { ensureCognitoToken } from '../utils/cognito-session.js';
16
- import { findHqRoot } from '../utils/hq-root.js';
16
+ import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
17
17
  import { getRegistryUrl, RegistryClient, } from '../utils/registry-client.js';
18
18
  import { verifySha256, verifyRsaSignature } from '../utils/integrity.js';
19
19
  import { readRegistry, addToRegistry, } from '../utils/registry.js';
@@ -32,7 +32,7 @@ export function registerPackageUpdateCommand(parent) {
32
32
  });
33
33
  }
34
34
  async function updatePackages(slug) {
35
- const hqRoot = findHqRoot();
35
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
36
36
  const entries = readRegistry(hqRoot);
37
37
  if (entries.length === 0) {
38
38
  console.log('No packages installed. Use "hq packages install <slug>" to install one.');
@@ -124,4 +124,4 @@ async function updatePackages(slug) {
124
124
  console.log(`\n${updatedCount} package(s) updated${updatedCount > 0 ? '.' : ' — everything is current.'}`);
125
125
  }
126
126
  //# sourceMappingURL=pkg-update.js.map
127
- //# debugId=3ef2968d-eb65-594b-a506-4314b70dd3dc
127
+ //# debugId=35fae6a0-f8e1-5fde-90d8-34ae4adb3c1c
@@ -10,13 +10,13 @@
10
10
  * 6. Gracefully handle conflicts (warn, don't force overwrite)
11
11
  */
12
12
 
13
- !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]="4c9a650e-05ed-5e33-b5de-b6d521ecdf4f")}catch(e){}}();
13
+ !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]="2b2504c1-06d5-59f8-a9b3-41d1af7f745f")}catch(e){}}();
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import { execSync } from 'child_process';
17
17
  import chalk from 'chalk';
18
18
  import simpleGit from 'simple-git';
19
- import { findHqRoot } from '../utils/hq-root.js';
19
+ import { resolveDefaultHqRoot } from '../utils/cognito-session.js';
20
20
  import { ensureCognitoToken } from '../utils/cognito-session.js';
21
21
  // ─── API helpers ────────────────────────────────────────────────────────────
22
22
  const API_BASE = 'https://example.com/api';
@@ -308,7 +308,7 @@ export function registerTeamSyncCommand(program) {
308
308
  .option('--dry-run', 'Show what would be synced without making changes')
309
309
  .action(async (options) => {
310
310
  try {
311
- const hqRoot = findHqRoot();
311
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
312
312
  // 1. Discover team directories
313
313
  let teamDirs = discoverTeamDirs(hqRoot);
314
314
  if (teamDirs.length === 0) {
@@ -422,4 +422,4 @@ export function registerTeamSyncCommand(program) {
422
422
  });
423
423
  }
424
424
  //# sourceMappingURL=team-sync.js.map
425
- //# debugId=4c9a650e-05ed-5e33-b5de-b6d521ecdf4f
425
+ //# debugId=2b2504c1-06d5-59f8-a9b3-41d1af7f745f
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
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]="a9b69c08-39a6-58c3-a4b6-a309d0ab8254")}catch(e){}}();
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]="0ed956d3-5fde-54aa-96cb-1859a15f7723")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -37,6 +37,7 @@ import { registerSourcesCommand } from "./commands/sources.js";
37
37
  import { registerSignalsCommand } from "./commands/signals.js";
38
38
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
39
39
  import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
40
+ import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
40
41
  import { CLI_VERSION } from "./cli-version.js";
41
42
  // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes the pipe early.
42
43
  const onPipeError = (err) => {
@@ -126,6 +127,14 @@ registerSignalsCommand(program);
126
127
  message: sanitizeArgv(process.argv.slice(2)).join(" "),
127
128
  level: "info",
128
129
  });
130
+ // Hard version gate: ask hq-pro whether this CLI is below the floor and
131
+ // auto-update if so (exits the process on update). Skipped for inspection
132
+ // flags (`--version`, `--help`) so users debugging a broken install can
133
+ // still introspect what they have. Silent on any failure — never blocks
134
+ // the CLI on a flaky network or hq-pro hiccup. See `utils/version-gate.ts`.
135
+ if (!shouldSkipGate(process.argv)) {
136
+ await enforceVersionGate();
137
+ }
129
138
  await program.parseAsync();
130
139
  }
131
140
  catch (err) {
@@ -137,4 +146,4 @@ registerSignalsCommand(program);
137
146
  }
138
147
  })();
139
148
  //# sourceMappingURL=index.js.map
140
- //# debugId=a9b69c08-39a6-58c3-a4b6-a309d0ab8254
149
+ //# debugId=0ed956d3-5fde-54aa-96cb-1859a15f7723
@@ -45,7 +45,28 @@ export declare const DEFAULT_VAULT_API_URL: string;
45
45
  * value at registration time, which matches the user's actual cwd at process
46
46
  * start. Re-importable as a function for tests and command-time resolution.
47
47
  */
48
- export declare function resolveDefaultHqRoot(): string;
48
+ /**
49
+ * Resolve the HQ root directory.
50
+ *
51
+ * Resolution order:
52
+ * 1. $HQ_ROOT env var (treated as an explicit assertion by the caller)
53
+ * 2. Walk up from cwd looking for `core.yaml` AND `companies/` siblings
54
+ * 3. Fall back to `~/hq` (or throw, per `opts.onMissing`)
55
+ *
56
+ * `opts.onMissing` controls the third arm:
57
+ * - `'fallback'` (default) — return `~/hq` if no HQ root is found above cwd.
58
+ * This preserves the module-load contract of `DEFAULT_HQ_ROOT`, which
59
+ * several commander.js `.option()` callers pin at registration time.
60
+ * - `'throw'` — throw with a user-actionable error. Used by module-management
61
+ * commands (pkg-install, pkg-remove, pkg-list, pkg-update, team-sync) where
62
+ * a silent default-path miss would silently target the wrong directory.
63
+ *
64
+ * `$HQ_ROOT` short-circuits both arms — if the env var is set, it's used
65
+ * as-is regardless of `onMissing`.
66
+ */
67
+ export declare function resolveDefaultHqRoot(opts?: {
68
+ onMissing?: "throw" | "fallback";
69
+ }): string;
49
70
  export declare const DEFAULT_HQ_ROOT: string;
50
71
  /**
51
72
  * Return a non-expired Cognito access token, refreshing or browser-logging-in