@indigoai-us/hq-cli 5.25.0 → 5.26.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.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Hard version-gate: ask hq-pro whether the current CLI is below the minimum
3
+ * acceptable version and, if so, run `npm install -g …@latest` synchronously
4
+ * before any commander parsing happens. Distinct from the existing
5
+ * `version-check.ts` which is a passive (cached, opt-in) stderr nag against
6
+ * the npm registry.
7
+ *
8
+ * Why both?
9
+ * - `version-check.ts` answers "is there something newer?" by polling npm
10
+ * directly. It's a soft hint, lives on a 24h cache, and never blocks.
11
+ * - `version-gate.ts` answers "is the team currently allowing your version
12
+ * to run?" via an authoritative hq-pro endpoint. The server can yank a
13
+ * known-bad release without waiting for the npm `latest` dist-tag move.
14
+ *
15
+ * The endpoint is reusable across clients (hq-sync, hq-installer, create-hq).
16
+ * See `apps/hq-pro/src/vault-service/handlers/client-version-check.ts` for the
17
+ * source-of-truth table.
18
+ *
19
+ * Trust model: anonymous. The CLI may be running pre-login (e.g. fresh
20
+ * install) so we never send credentials. The endpoint identifies the client
21
+ * by `clientId` + `currentVersion`.
22
+ *
23
+ * Failure mode: silent. Network down, hq-pro returning 5xx, malformed body —
24
+ * the gate must never break the CLI for a user who's otherwise fine. We log
25
+ * to Sentry as a breadcrumb (best-effort) and return.
26
+ *
27
+ * Opt-out: `HQ_NO_UPDATE_CHECK=1` (same env as `version-check.ts` — one knob
28
+ * to silence both check + gate).
29
+ */
30
+
31
+ !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]="d7d58261-f0e4-56bc-b6eb-4117731744f9")}catch(e){}}();
32
+ import { spawnSync } from "node:child_process";
33
+ import chalk from "chalk";
34
+ import { CLI_VERSION } from "../cli-version.js";
35
+ import { DEFAULT_VAULT_API_URL } from "./cognito-session.js";
36
+ const CLIENT_ID = "hq-cli";
37
+ const ENDPOINT_PATH = "/v1/client-version/check";
38
+ const FETCH_TIMEOUT_MS = 3_000;
39
+ function isOptedOut() {
40
+ return process.env.HQ_NO_UPDATE_CHECK === "1";
41
+ }
42
+ /**
43
+ * Hit POST /v1/client-version/check. Returns the parsed body on 200, or
44
+ * `null` on any failure (caller treats as "no gate"). Tight 3s timeout —
45
+ * a hung server must not delay CLI startup.
46
+ */
47
+ async function fetchVersionDecision() {
48
+ try {
49
+ const url = `${DEFAULT_VAULT_API_URL}${ENDPOINT_PATH}`;
50
+ const res = await fetch(url, {
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ Accept: "application/json",
55
+ },
56
+ body: JSON.stringify({
57
+ clientId: CLIENT_ID,
58
+ currentVersion: CLI_VERSION,
59
+ platform: `${process.platform}-${process.arch}`,
60
+ }),
61
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
62
+ });
63
+ if (!res.ok)
64
+ return null;
65
+ const body = (await res.json());
66
+ if (typeof body.minVersion !== "string" ||
67
+ typeof body.latestVersion !== "string" ||
68
+ typeof body.updateRequired !== "boolean") {
69
+ return null;
70
+ }
71
+ return body;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ /**
78
+ * Run the upgrade command in a blocking subprocess. Inherits stdio so the
79
+ * user sees the npm progress. We do NOT auto-rerun the CLI on completion —
80
+ * forcing a re-invocation would run twice on the same process and feel
81
+ * janky; instead we print a clear "rerun your command" message and exit.
82
+ */
83
+ function performUpdate(command) {
84
+ const parts = command.split(/\s+/).filter(Boolean);
85
+ if (parts.length === 0)
86
+ return { ok: false, detail: "empty command" };
87
+ const cmd = parts[0];
88
+ const args = parts.slice(1);
89
+ try {
90
+ const result = spawnSync(cmd, args, { stdio: "inherit" });
91
+ if (result.status !== 0) {
92
+ return {
93
+ ok: false,
94
+ detail: `exit ${result.status ?? "signal"}`,
95
+ };
96
+ }
97
+ return { ok: true };
98
+ }
99
+ catch (err) {
100
+ return { ok: false, detail: err instanceof Error ? err.message : String(err) };
101
+ }
102
+ }
103
+ /**
104
+ * Soft notify when the server says we're below `latestVersion` but still ≥
105
+ * `minVersion`. Single chalk-yellow line on stderr; never blocks.
106
+ */
107
+ function nudgeUpdateRecommended(decision) {
108
+ const msg = chalk.yellow(`⚠ A new version of hq-cli is available: ${decision.latestVersion} (current: ${decision.currentVersion}).`);
109
+ console.error(msg);
110
+ if (decision.updateCommand) {
111
+ console.error(chalk.dim(` Update: ${decision.updateCommand}`));
112
+ }
113
+ }
114
+ /**
115
+ * Hard enforcement when the server says we're below `minVersion`. Print a
116
+ * red banner, attempt the update, then exit so the user reruns against the
117
+ * fresh binary. Sequence chosen so a user with a broken `npm` global prefix
118
+ * still gets a clear error rather than an opaque silent failure.
119
+ *
120
+ * Exit codes:
121
+ * 0 — update succeeded; user must rerun their command
122
+ * 75 — update failed (EX_TEMPFAIL; common for sudo/EACCES on system npm)
123
+ */
124
+ function enforceUpdateRequired(decision) {
125
+ const banner = chalk.red.bold(`✗ hq-cli ${decision.currentVersion} is below the minimum required version (${decision.minVersion}).`);
126
+ console.error(banner);
127
+ if (decision.message)
128
+ console.error(chalk.dim(` ${decision.message}`));
129
+ const command = decision.updateCommand;
130
+ if (!command) {
131
+ console.error(chalk.red(" No updateCommand provided by hq-pro — see https://hq.indigo.ai/docs/cli-update for manual steps."));
132
+ if (decision.downloadUrl) {
133
+ console.error(chalk.dim(` Download: ${decision.downloadUrl}`));
134
+ }
135
+ process.exit(75);
136
+ }
137
+ console.error(chalk.dim(` Running: ${command}`));
138
+ const result = performUpdate(command);
139
+ if (!result.ok) {
140
+ console.error(chalk.red(`✗ Update failed${result.detail ? `: ${result.detail}` : ""}.`));
141
+ console.error(chalk.dim(` Try manually: ${command}`));
142
+ process.exit(75);
143
+ }
144
+ console.error(chalk.green(`✓ Updated to hq-cli ${decision.latestVersion}. Rerun your command.`));
145
+ process.exit(0);
146
+ }
147
+ /**
148
+ * Public entry point. Call before commander parses argv. Blocks the CLI on
149
+ * network IO for up to FETCH_TIMEOUT_MS — acceptable because the alternative
150
+ * (a fire-and-forget background check) gives the user no chance to bail out
151
+ * of a known-bad version before it does damage.
152
+ *
153
+ * `--version` / `-v` callers MUST skip the gate (the user is debugging a
154
+ * broken install and shouldn't be force-upgraded mid-investigation). Caller
155
+ * is responsible for checking argv before invoking us — see index.ts.
156
+ */
157
+ export async function enforceVersionGate() {
158
+ if (isOptedOut())
159
+ return;
160
+ const decision = await fetchVersionDecision();
161
+ if (!decision)
162
+ return; // best-effort: silent on any failure
163
+ if (decision.updateRequired) {
164
+ enforceUpdateRequired(decision); // exits process
165
+ }
166
+ if (decision.updateRecommended) {
167
+ nudgeUpdateRecommended(decision);
168
+ }
169
+ }
170
+ /**
171
+ * Cheap argv pre-check: skip the gate for `--version` / `-V` so users
172
+ * inspecting a broken install can still see what they have without being
173
+ * force-upgraded.
174
+ */
175
+ export function shouldSkipGate(argv) {
176
+ return argv.some((a) => a === "--version" || a === "-V" || a === "-v" || a === "--help" || a === "-h");
177
+ }
178
+ export const __test__ = {
179
+ CLIENT_ID,
180
+ ENDPOINT_PATH,
181
+ FETCH_TIMEOUT_MS,
182
+ performUpdate,
183
+ };
184
+ //# sourceMappingURL=version-gate.js.map
185
+ //# debugId=d7d58261-f0e4-56bc-b6eb-4117731744f9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.25.0",
3
+ "version": "5.26.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { detectRecipient, parseDuration, buildDmBody } from "./dm.js";
3
+
4
+ describe("detectRecipient", () => {
5
+ it("classifies an email", () => {
6
+ expect(detectRecipient("Stefan@Getindigo.ai")).toEqual({
7
+ toEmail: "stefan@getindigo.ai",
8
+ });
9
+ });
10
+ it("classifies a personUid", () => {
11
+ expect(detectRecipient("prs_01ABC")).toEqual({ toPersonUid: "prs_01ABC" });
12
+ });
13
+ it("rejects anything else", () => {
14
+ expect(detectRecipient("not-an-email")).toBeNull();
15
+ expect(detectRecipient("")).toBeNull();
16
+ });
17
+ });
18
+
19
+ describe("parseDuration", () => {
20
+ it("parses units", () => {
21
+ expect(parseDuration("30s")).toBe(30_000);
22
+ expect(parseDuration("10m")).toBe(600_000);
23
+ expect(parseDuration("2h")).toBe(7_200_000);
24
+ expect(parseDuration("1d")).toBe(86_400_000);
25
+ expect(parseDuration(" 5m ")).toBe(300_000);
26
+ });
27
+ it("returns null on garbage", () => {
28
+ expect(parseDuration("soon")).toBeNull();
29
+ expect(parseDuration("10")).toBeNull();
30
+ expect(parseDuration("10x")).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe("buildDmBody", () => {
35
+ const now = Date.parse("2026-05-29T00:00:00.000Z");
36
+
37
+ it("builds an email DM with body", () => {
38
+ expect(
39
+ buildDmBody({ recipient: "a@b.com", message: " hi ", now }),
40
+ ).toEqual({ toEmail: "a@b.com", body: "hi" });
41
+ });
42
+
43
+ it("includes prompt + details when present, omits when blank", () => {
44
+ expect(
45
+ buildDmBody({
46
+ recipient: "prs_x",
47
+ message: "m",
48
+ prompt: "do the thing",
49
+ details: " ",
50
+ now,
51
+ }),
52
+ ).toEqual({ toPersonUid: "prs_x", body: "m", prompt: "do the thing" });
53
+ });
54
+
55
+ it("resolves --in to a future deliverAt", () => {
56
+ const out = buildDmBody({ recipient: "a@b.com", message: "m", inDelay: "10m", now });
57
+ expect(out.deliverAt).toBe("2026-05-29T00:10:00.000Z");
58
+ });
59
+
60
+ it("resolves --at to a normalized ISO deliverAt", () => {
61
+ const out = buildDmBody({
62
+ recipient: "a@b.com",
63
+ message: "m",
64
+ at: "2026-06-01T12:00:00Z",
65
+ now,
66
+ });
67
+ expect(out.deliverAt).toBe("2026-06-01T12:00:00.000Z");
68
+ });
69
+
70
+ it("rejects an invalid recipient", () => {
71
+ expect(() => buildDmBody({ recipient: "nope", message: "m", now })).toThrow(/Invalid recipient/);
72
+ });
73
+
74
+ it("requires a body", () => {
75
+ expect(() => buildDmBody({ recipient: "a@b.com", message: " ", now })).toThrow(/body is required/);
76
+ });
77
+
78
+ it("rejects both --at and --in", () => {
79
+ expect(() =>
80
+ buildDmBody({ recipient: "a@b.com", message: "m", at: "2026-06-01T12:00:00Z", inDelay: "10m", now }),
81
+ ).toThrow(/only one of --at or --in/);
82
+ });
83
+
84
+ it("rejects an invalid --at and --in", () => {
85
+ expect(() => buildDmBody({ recipient: "a@b.com", message: "m", at: "nope", now })).toThrow(/Invalid --at/);
86
+ expect(() => buildDmBody({ recipient: "a@b.com", message: "m", inDelay: "soon", now })).toThrow(/Invalid --in/);
87
+ });
88
+ });
@@ -0,0 +1,222 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { readFileSync } from "node:fs";
4
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
5
+ import { vaultApiFetch } from "../utils/vault-api.js";
6
+
7
+ const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
8
+ const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
9
+
10
+ export interface DmRecipient {
11
+ toEmail?: string;
12
+ toPersonUid?: string;
13
+ }
14
+
15
+ /**
16
+ * Classify a recipient arg as an email or a personUid. Mirrors the
17
+ * email/prs_ heuristic used by `hq members`. Returns null for neither.
18
+ */
19
+ export function detectRecipient(recipient: string): DmRecipient | null {
20
+ const r = recipient.trim();
21
+ if (EMAIL_PATTERN.test(r)) return { toEmail: r.toLowerCase() };
22
+ if (PERSON_UID_PATTERN.test(r)) return { toPersonUid: r };
23
+ return null;
24
+ }
25
+
26
+ /**
27
+ * Parse a relative duration like `30s`, `10m`, `2h`, `1d` into milliseconds.
28
+ * Returns null on anything that doesn't match. Pure → unit-testable.
29
+ */
30
+ export function parseDuration(input: string): number | null {
31
+ const m = /^(\d+)\s*(s|m|h|d)$/.exec(input.trim());
32
+ if (!m) return null;
33
+ const n = parseInt(m[1], 10);
34
+ const mult: Record<string, number> = {
35
+ s: 1000,
36
+ m: 60_000,
37
+ h: 3_600_000,
38
+ d: 86_400_000,
39
+ };
40
+ return n * mult[m[2]];
41
+ }
42
+
43
+ export interface DmSendBody {
44
+ toEmail?: string;
45
+ toPersonUid?: string;
46
+ body: string;
47
+ prompt?: string;
48
+ details?: string;
49
+ deliverAt?: string;
50
+ }
51
+
52
+ /**
53
+ * Build the POST /v1/notify/dm request body from CLI inputs. Pure (no I/O,
54
+ * no clock) so the option-resolution logic is unit-testable; the caller
55
+ * supplies `now` for the `--in` relative-delay computation.
56
+ *
57
+ * Throws Error with a user-facing message on invalid input.
58
+ */
59
+ export function buildDmBody(args: {
60
+ recipient: string;
61
+ message: string;
62
+ prompt?: string;
63
+ details?: string;
64
+ at?: string;
65
+ inDelay?: string;
66
+ now: number;
67
+ }): DmSendBody {
68
+ const rcpt = detectRecipient(args.recipient);
69
+ if (!rcpt) {
70
+ throw new Error(
71
+ `Invalid recipient '${args.recipient}': must be an email address or a personUid (prs_…).`,
72
+ );
73
+ }
74
+ const body = (args.message ?? "").trim();
75
+ if (!body) {
76
+ throw new Error("A message body is required: hq dm <recipient> <message>");
77
+ }
78
+
79
+ if (args.at && args.inDelay) {
80
+ throw new Error("Use only one of --at or --in, not both.");
81
+ }
82
+ let deliverAt: string | undefined;
83
+ if (args.at) {
84
+ const when = new Date(args.at);
85
+ if (isNaN(when.getTime())) {
86
+ throw new Error(`Invalid --at '${args.at}': must be an ISO8601 date.`);
87
+ }
88
+ deliverAt = when.toISOString();
89
+ } else if (args.inDelay) {
90
+ const ms = parseDuration(args.inDelay);
91
+ if (ms === null) {
92
+ throw new Error(
93
+ `Invalid --in '${args.inDelay}': use a relative delay like 30s, 10m, 2h, 1d.`,
94
+ );
95
+ }
96
+ deliverAt = new Date(args.now + ms).toISOString();
97
+ }
98
+
99
+ const prompt = args.prompt?.trim();
100
+ const details = args.details?.trim();
101
+
102
+ return {
103
+ ...rcpt,
104
+ body,
105
+ ...(prompt ? { prompt } : {}),
106
+ ...(details ? { details } : {}),
107
+ ...(deliverAt ? { deliverAt } : {}),
108
+ };
109
+ }
110
+
111
+ function friendlyDmError(status: number, code: string | undefined, fallback: string): string {
112
+ if (status === 401) return "Not authenticated — run `hq login` and try again.";
113
+ if (status === 404 || code === "RECIPIENT_NOT_FOUND") {
114
+ return "Recipient not found or not reachable — you can only DM someone you share an active company with.";
115
+ }
116
+ if (status >= 500) return `Server error: ${fallback}`;
117
+ return fallback;
118
+ }
119
+
120
+ export function registerDmCommand(program: Command): void {
121
+ program
122
+ .command("dm <recipient> [message]")
123
+ .description(
124
+ "Send a direct message to a teammate (email or personUid). They receive it as an HQ Sync notification.",
125
+ )
126
+ .option(
127
+ "--prompt <text>",
128
+ "Agent-context prompt the recipient can one-click copy into their agent",
129
+ )
130
+ .option("--prompt-file <path>", "Read the agent prompt from a file")
131
+ .option(
132
+ "--details <text>",
133
+ "Longer detail shown in the recipient's DM detail window",
134
+ )
135
+ .option("--details-file <path>", "Read the details from a file")
136
+ .option(
137
+ "--at <iso>",
138
+ "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time)",
139
+ )
140
+ .option(
141
+ "--in <duration>",
142
+ "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d",
143
+ )
144
+ .action(
145
+ async (
146
+ recipient: string,
147
+ message: string | undefined,
148
+ opts: {
149
+ prompt?: string;
150
+ promptFile?: string;
151
+ details?: string;
152
+ detailsFile?: string;
153
+ at?: string;
154
+ in?: string;
155
+ },
156
+ ) => {
157
+ try {
158
+ // Resolve prompt/details from inline text or a file.
159
+ let prompt = opts.prompt;
160
+ if (opts.promptFile) prompt = readFileSync(opts.promptFile, "utf8");
161
+ let details = opts.details;
162
+ if (opts.detailsFile) details = readFileSync(opts.detailsFile, "utf8");
163
+
164
+ const reqBody = buildDmBody({
165
+ recipient,
166
+ message: message ?? "",
167
+ prompt,
168
+ details,
169
+ at: opts.at,
170
+ inDelay: opts.in,
171
+ now: Date.now(),
172
+ });
173
+
174
+ const token = await ensureCognitoToken();
175
+ const res = await vaultApiFetch({
176
+ token,
177
+ path: "/v1/notify/dm",
178
+ method: "POST",
179
+ body: reqBody as unknown as Record<string, unknown>,
180
+ });
181
+
182
+ if (!res.ok) {
183
+ const err = (await res.json().catch(() => ({}))) as Record<string, string>;
184
+ console.error(
185
+ chalk.red(
186
+ friendlyDmError(res.status, err.code, err.error ?? err.message ?? res.statusText),
187
+ ),
188
+ );
189
+ process.exit(1);
190
+ }
191
+
192
+ const data = (await res.json()) as {
193
+ eventId?: string;
194
+ createdAt?: string;
195
+ scheduled?: boolean;
196
+ deliverAt?: string;
197
+ };
198
+
199
+ if (data.scheduled) {
200
+ console.log(
201
+ chalk.green(
202
+ `Scheduled DM to ${recipient} for ${data.deliverAt} (eventId ${data.eventId}).`,
203
+ ),
204
+ );
205
+ console.log(
206
+ chalk.dim("It delivers within ~60s of that time, even if you're offline."),
207
+ );
208
+ } else {
209
+ console.log(
210
+ chalk.green(`DM sent to ${recipient} (eventId ${data.eventId}).`),
211
+ );
212
+ }
213
+ } catch (err) {
214
+ console.error(
215
+ chalk.red("Error:"),
216
+ err instanceof Error ? err.message : String(err),
217
+ );
218
+ process.exit(1);
219
+ }
220
+ },
221
+ );
222
+ }
@@ -21,7 +21,7 @@ import { execSync } from 'child_process';
21
21
  import { Command } from 'commander';
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 {
26
26
  getRegistryUrl,
27
27
  RegistryClient,
@@ -124,7 +124,7 @@ async function installPackage(
124
124
  }
125
125
 
126
126
  // 7. Extract
127
- const hqRoot = findHqRoot();
127
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
128
128
  const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
129
129
 
130
130
  // Clean existing installation
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { Command } from 'commander';
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 {
@@ -34,7 +34,7 @@ export function registerPackageListCommand(parent: Command): void {
34
34
  }
35
35
 
36
36
  async function listPackages(): Promise<void> {
37
- const hqRoot = findHqRoot();
37
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
38
38
  const installed = readRegistry(hqRoot);
39
39
 
40
40
  // Print installed packages
@@ -10,7 +10,7 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { Command } from 'commander';
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
 
16
16
  export function registerPackageRemoveCommand(parent: Command): void {
@@ -31,7 +31,7 @@ export function registerPackageRemoveCommand(parent: Command): void {
31
31
  }
32
32
 
33
33
  async function removePackage(slug: string): Promise<void> {
34
- const hqRoot = findHqRoot();
34
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
35
35
  const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
36
36
 
37
37
  // Verify it is actually installed
@@ -13,7 +13,7 @@ import { execSync } from 'child_process';
13
13
  import { Command } from 'commander';
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 {
18
18
  getRegistryUrl,
19
19
  RegistryClient,
@@ -43,7 +43,7 @@ export function registerPackageUpdateCommand(parent: Command): void {
43
43
  }
44
44
 
45
45
  async function updatePackages(slug?: string): Promise<void> {
46
- const hqRoot = findHqRoot();
46
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
47
47
  const entries = readRegistry(hqRoot);
48
48
 
49
49
  if (entries.length === 0) {
@@ -16,7 +16,7 @@ import { execSync } from 'child_process';
16
16
  import { Command } from 'commander';
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
 
22
22
  // ─── Types ──────────────────────────────────────────────────────────────────
@@ -433,7 +433,7 @@ export function registerTeamSyncCommand(program: Command): void {
433
433
  .action(
434
434
  async (options: { team?: string; dryRun?: boolean }) => {
435
435
  try {
436
- const hqRoot = findHqRoot();
436
+ const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
437
437
 
438
438
  // 1. Discover team directories
439
439
  let teamDirs = discoverTeamDirs(hqRoot);
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import { registerGroupsCommand } from "./commands/groups.js";
31
31
  import { registerFilesCommand } from "./commands/files.js";
32
32
  import { registerFilesBrowseCommands } from "./commands/files-browse.js";
33
33
  import { registerMembersCommand } from "./commands/members.js";
34
+ import { registerDmCommand } from "./commands/dm.js";
34
35
  import { registerFeedbackCommand } from "./commands/feedback.js";
35
36
  import { registerMeetingsCommand } from "./commands/meetings.js";
36
37
  import { registerSourcesCommand } from "./commands/sources.js";
@@ -40,6 +41,10 @@ import {
40
41
  maybeWarnNewVersion,
41
42
  refreshVersionCache,
42
43
  } from "./utils/version-check.js";
44
+ import {
45
+ enforceVersionGate,
46
+ shouldSkipGate,
47
+ } from "./utils/version-gate.js";
43
48
  import { CLI_VERSION } from "./cli-version.js";
44
49
 
45
50
  // Swallow EPIPE when a downstream reader (e.g. `source <(…)`, `| head`) closes the pipe early.
@@ -134,6 +139,7 @@ registerFilesBrowseCommands(filesCmd);
134
139
 
135
140
  // Membership management (subcommand group — hq members invite|list|revoke)
136
141
  registerMembersCommand(program);
142
+ registerDmCommand(program);
137
143
 
138
144
  // Onboarding (top-level — Cognito + vault-service provisioning)
139
145
  registerOnboardCommand(program);
@@ -157,6 +163,14 @@ registerSignalsCommand(program);
157
163
  message: sanitizeArgv(process.argv.slice(2)).join(" "),
158
164
  level: "info",
159
165
  });
166
+ // Hard version gate: ask hq-pro whether this CLI is below the floor and
167
+ // auto-update if so (exits the process on update). Skipped for inspection
168
+ // flags (`--version`, `--help`) so users debugging a broken install can
169
+ // still introspect what they have. Silent on any failure — never blocks
170
+ // the CLI on a flaky network or hq-pro hiccup. See `utils/version-gate.ts`.
171
+ if (!shouldSkipGate(process.argv)) {
172
+ await enforceVersionGate();
173
+ }
160
174
  await program.parseAsync();
161
175
  } catch (err) {
162
176
  Sentry.captureException(err);