@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.
- package/.github/workflows/publish.yml +21 -1
- package/dist/commands/dm.d.ts +41 -0
- package/dist/commands/dm.js +148 -0
- package/dist/commands/pkg-install.js +4 -4
- package/dist/commands/pkg-list.js +4 -4
- package/dist/commands/pkg-remove.js +4 -4
- package/dist/commands/pkg-update.js +4 -4
- package/dist/commands/team-sync.js +4 -4
- package/dist/index.js +13 -2
- package/dist/utils/cognito-session.d.ts +22 -1
- package/dist/utils/cognito-session.js +26 -3
- package/dist/utils/integrity.js +4 -4
- package/dist/utils/registry-client.js +4 -4
- package/dist/utils/version-gate.d.ts +64 -0
- package/dist/utils/version-gate.js +185 -0
- package/package.json +1 -1
- package/src/commands/dm.test.ts +88 -0
- package/src/commands/dm.ts +222 -0
- package/src/commands/pkg-install.ts +2 -2
- package/src/commands/pkg-list.ts +2 -2
- package/src/commands/pkg-remove.ts +2 -2
- package/src/commands/pkg-update.ts +2 -2
- package/src/commands/team-sync.ts +2 -2
- package/src/index.ts +14 -0
- package/src/utils/cognito-session.test.ts +56 -1
- package/src/utils/cognito-session.ts +28 -1
- package/src/utils/integrity.ts +2 -2
- package/src/utils/registry-client.ts +2 -2
- package/src/utils/version-gate.test.ts +245 -0
- package/src/utils/version-gate.ts +219 -0
- package/dist/utils/hq-root.d.ts +0 -10
- package/dist/utils/hq-root.js +0 -25
- package/src/utils/hq-root.ts +0 -27
|
@@ -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
|
@@ -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 {
|
|
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 =
|
|
127
|
+
const hqRoot = resolveDefaultHqRoot({ onMissing: 'throw' });
|
|
128
128
|
const installDir = path.resolve(hqRoot, 'packages', 'installed', slug);
|
|
129
129
|
|
|
130
130
|
// Clean existing installation
|
package/src/commands/pkg-list.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
-
import {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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);
|