@indigoai-us/hq-cli 5.322.0 → 5.323.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/CHANGELOG.md +4 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +263 -0
- package/assets/scaffold/core/scripts/lib/transcript-tail.sh +98 -0
- package/assets/scaffold/core/scripts/lib/trigger-fact-text.awk +86 -0
- package/dist/command-catalog.generated.d.ts +13 -0
- package/dist/command-catalog.generated.js +16 -0
- package/dist/commands/cloud.js +23 -4
- package/dist/commands/core-derive-trigger-facts-command.d.ts +3 -0
- package/dist/commands/core-derive-trigger-facts-command.js +47 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +21 -0
- package/dist/commands/core-derive-trigger-facts-native.js +492 -0
- package/dist/commands/core.js +6 -0
- package/dist/fast-scaffold.js +13 -0
- package/dist/lib/cli-invite-nudge.d.ts +25 -0
- package/dist/lib/cli-invite-nudge.js +152 -0
- package/dist/lib/first-sync-next-step.d.ts +10 -1
- package/dist/main.js +3 -1
- package/dist/utils/cli-telemetry.d.ts +11 -0
- package/dist/utils/cli-telemetry.js +76 -0
- package/dist/utils/run-bundled-script.d.ts +7 -4
- package/dist/utils/run-bundled-script.js +47 -12
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { isExpiring, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
7
|
+
import { queueCliInviteNudgeEvent } from "../utils/cli-telemetry.js";
|
|
8
|
+
import { resolveCompanyFlagGate } from "./flag-registry.js";
|
|
9
|
+
import { isHumanInteractiveCliSession } from "./first-sync-next-step.js";
|
|
10
|
+
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
11
|
+
export const CLI_INVITE_NUDGE_FLAG = "cli.first-sync-invite-teammate-v1";
|
|
12
|
+
function reportFailure(operation, error) {
|
|
13
|
+
const name = error instanceof Error ? error.name : "unknown";
|
|
14
|
+
const code = /^[A-Za-z][A-Za-z0-9]{0,39}$/.test(name) ? name : "unknown";
|
|
15
|
+
console.error(`hq: Could not ${operation} (${code}); invite prompt stays hidden.`);
|
|
16
|
+
}
|
|
17
|
+
/** One eligible, successful work-bearing company sync can show one prompt per company. */
|
|
18
|
+
export async function runCliInviteNudge(input) {
|
|
19
|
+
if (!input.interactive || !input.canPrompt || input.alreadyShown || !/^cmp_[A-Za-z0-9_-]{1,128}$/.test(input.companyUid)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
let enabled;
|
|
23
|
+
try {
|
|
24
|
+
enabled = await input.flagEnabled();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
(input.reportFailure ?? reportFailure)("resolve the invite flag", error);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (!enabled)
|
|
31
|
+
return false;
|
|
32
|
+
let humanMemberCount;
|
|
33
|
+
try {
|
|
34
|
+
humanMemberCount = await input.readHumanMemberCount();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
(input.reportFailure ?? reportFailure)("check company membership", error);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (humanMemberCount !== 1 || !input.claimShown())
|
|
41
|
+
return false;
|
|
42
|
+
input.write("Your company has one member. Invite a teammate to share the workspace.");
|
|
43
|
+
input.record(input.companyUid, "shown");
|
|
44
|
+
let clicked;
|
|
45
|
+
try {
|
|
46
|
+
clicked = await input.ask("Show the existing invite command? [y/N] ");
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
(input.reportFailure ?? reportFailure)("read the invite choice", error);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
input.record(input.companyUid, clicked ? "clicked" : "dismissed");
|
|
53
|
+
if (clicked) {
|
|
54
|
+
input.write(`Run: hq members invite <email> --company ${input.companyRef} --role member`);
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function inviteNudgeMarkerPath(companyUid, home = process.env.HOME?.trim() || os.homedir()) {
|
|
59
|
+
const digest = createHash("sha256").update(companyUid).digest("hex");
|
|
60
|
+
return path.join(home, ".hq", "cli-invite-nudge", `${digest}.shown`);
|
|
61
|
+
}
|
|
62
|
+
function markerExists(companyUid) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.existsSync(inviteNudgeMarkerPath(companyUid));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
reportFailure("read invite prompt state", error);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function claimMarker(companyUid) {
|
|
72
|
+
const marker = inviteNudgeMarkerPath(companyUid);
|
|
73
|
+
const homeHq = path.join(process.env.HOME?.trim() || os.homedir(), ".hq");
|
|
74
|
+
try {
|
|
75
|
+
// Do not create ~/.hq for a machine without a signed-in person profile.
|
|
76
|
+
if (!fs.existsSync(homeHq))
|
|
77
|
+
return false;
|
|
78
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true, mode: 0o700 });
|
|
79
|
+
fs.writeFileSync(marker, `${new Date().toISOString()}\n`, { flag: "wx", mode: 0o600 });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
reportFailure("write invite prompt state", error);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function readActiveHumanMemberCount(accessToken, companyUid) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeout = setTimeout(() => controller.abort(), 1_200);
|
|
93
|
+
try {
|
|
94
|
+
const response = await vaultApiFetch({
|
|
95
|
+
token: accessToken,
|
|
96
|
+
path: `/membership/company/${encodeURIComponent(companyUid)}`,
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
throw new Error(`http_${response.status}`);
|
|
101
|
+
const body = await response.json();
|
|
102
|
+
if (!Array.isArray(body.members))
|
|
103
|
+
throw new Error("invalid_roster_shape");
|
|
104
|
+
return body.members.filter((member) => typeof member === "object" && member !== null &&
|
|
105
|
+
member.status === "active" &&
|
|
106
|
+
typeof member.personUid === "string" &&
|
|
107
|
+
member.personUid.startsWith("prs_")).length;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function askInviteChoice(prompt) {
|
|
114
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
115
|
+
try {
|
|
116
|
+
const answer = await terminal.question(prompt);
|
|
117
|
+
return answer.trim().toLowerCase() === "y";
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
terminal.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Called after a successful work-bearing company sync; every dependency remains fail-closed. */
|
|
124
|
+
export async function maybeShowCliInviteNudge(input) {
|
|
125
|
+
const companyUid = input.companyUid?.trim() ?? "";
|
|
126
|
+
const companyRef = input.companyRef?.trim() ?? "";
|
|
127
|
+
if (!companyUid || !companyRef)
|
|
128
|
+
return false;
|
|
129
|
+
const cached = loadCachedTokens();
|
|
130
|
+
if (!cached || isExpiring(cached, 120) ||
|
|
131
|
+
!isHumanInteractiveCliSession({
|
|
132
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
133
|
+
argv: process.argv,
|
|
134
|
+
env: process.env,
|
|
135
|
+
machineIdentity: isMachineIdentity(),
|
|
136
|
+
}))
|
|
137
|
+
return false;
|
|
138
|
+
return runCliInviteNudge({
|
|
139
|
+
companyUid,
|
|
140
|
+
companyRef,
|
|
141
|
+
interactive: true,
|
|
142
|
+
canPrompt: process.stdin.isTTY === true,
|
|
143
|
+
alreadyShown: markerExists(companyUid),
|
|
144
|
+
flagEnabled: () => resolveCompanyFlagGate(companyUid, cached.idToken ?? "", CLI_INVITE_NUDGE_FLAG, { fallback: false }, () => false),
|
|
145
|
+
readHumanMemberCount: () => readActiveHumanMemberCount(input.accessToken, companyUid),
|
|
146
|
+
claimShown: () => claimMarker(companyUid),
|
|
147
|
+
ask: askInviteChoice,
|
|
148
|
+
write: (line) => console.log(line),
|
|
149
|
+
record: queueCliInviteNudgeEvent,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=cli-invite-nudge.js.map
|
|
@@ -12,7 +12,15 @@ export declare function shouldConfirmFirstWorkBearingSync(input: {
|
|
|
12
12
|
alreadyCompleted: boolean;
|
|
13
13
|
workBearing: boolean;
|
|
14
14
|
}): boolean;
|
|
15
|
-
|
|
15
|
+
interface WorkBearingSyncResult {
|
|
16
|
+
aborted?: unknown;
|
|
17
|
+
filesUploaded?: unknown;
|
|
18
|
+
filesDownloaded?: unknown;
|
|
19
|
+
sessionLogUploadedCount?: unknown;
|
|
20
|
+
filesDeleted?: unknown;
|
|
21
|
+
tombstonedCount?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export declare function isWorkBearingSyncResult(result: WorkBearingSyncResult): boolean;
|
|
16
24
|
export declare function isHumanInteractiveCliSession(input: {
|
|
17
25
|
stdoutIsTTY: boolean;
|
|
18
26
|
argv: readonly string[];
|
|
@@ -31,4 +39,5 @@ export declare function maybeShowFirstSyncNextStep(): boolean;
|
|
|
31
39
|
export declare function recordFirstWorkBearingSync(result: Record<string, unknown>): boolean;
|
|
32
40
|
/** Drain outcomes once so each shown/followed event is sent at most once per run. */
|
|
33
41
|
export declare function takeFirstSyncStepOutcomes(): FirstSyncStepOutcome[];
|
|
42
|
+
export {};
|
|
34
43
|
//# sourceMappingURL=first-sync-next-step.d.ts.map
|
package/dist/main.js
CHANGED
|
@@ -44,7 +44,7 @@ import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
|
|
|
44
44
|
import { autoUpdateAndReexec } from "./utils/self-update.js";
|
|
45
45
|
import { CLI_VERSION } from "./cli-version.js";
|
|
46
46
|
import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
|
|
47
|
-
import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
|
|
47
|
+
import { emitCliFirstSyncNextStepOutcome, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
|
|
48
48
|
import { reportCliClientHealthInvocation } from "./utils/client-health.js";
|
|
49
49
|
import { settleWithin } from "./utils/settle-with-timeout.js";
|
|
50
50
|
import { emitPlanLimitNag } from "./lib/plan-limit-nag.js";
|
|
@@ -313,6 +313,7 @@ export async function runCli() {
|
|
|
313
313
|
// slice even when the command itself was long-running.
|
|
314
314
|
const releaseHealthTimeoutMs = foregroundNetworkBudget.timeoutFor(FINALIZATION_FOREGROUND_WAIT_MS);
|
|
315
315
|
const firstSyncOutcomeFlushes = takeFirstSyncStepOutcomes().map((outcome) => emitCliFirstSyncNextStepOutcome(outcome, releaseHealthTimeoutMs));
|
|
316
|
+
const inviteNudgeOutcomeFlushes = takeCliInviteNudgeEvents().map((event) => emitCliInviteNudgeEvent(event, releaseHealthTimeoutMs));
|
|
316
317
|
const inviteTeammateExposureFlushes = takePlanLimitInviteTeammateExposures().map((companyUid) => emitPlanLimitInviteTeammateExposure(companyUid, releaseHealthTimeoutMs));
|
|
317
318
|
const limitNoticeExposureFlushes = takeCliLimitNoticeExposures().map((exposure) => emitCliLimitNoticeExposure(exposure, releaseHealthTimeoutMs));
|
|
318
319
|
const limitNoticeEngagementFlushes = takeCliLimitNoticeEngagements().map((engagement) => emitCliLimitNoticeEngagement(engagement, releaseHealthTimeoutMs));
|
|
@@ -322,6 +323,7 @@ export async function runCli() {
|
|
|
322
323
|
: [refreshVersionCache({ timeoutMs: releaseHealthTimeoutMs })]),
|
|
323
324
|
Sentry.flush(releaseHealthTimeoutMs),
|
|
324
325
|
...firstSyncOutcomeFlushes,
|
|
326
|
+
...inviteNudgeOutcomeFlushes,
|
|
325
327
|
...inviteTeammateExposureFlushes,
|
|
326
328
|
...limitNoticeExposureFlushes,
|
|
327
329
|
...limitNoticeEngagementFlushes,
|
|
@@ -9,8 +9,14 @@ export declare const TELEMETRY_TIMEOUT_MS = 1200;
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp, never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
|
|
11
11
|
export declare const FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp; first-sync guidance shown or followed, with the identifier cli.first-sync-next-step; never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
|
|
12
|
+
export declare const CLI_INVITE_NUDGE_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when it shows the teammate invite prompt (your company UID and whether you saw, clicked, or dismissed the prompt; no invitee details, command names, file contents, or secrets). Set HQ_TELEMETRY=0 to turn this off.";
|
|
12
13
|
export declare const CLI_FEATURE_USED_TELEMETRY_FLAG = "cli.feature-used-telemetry-v1";
|
|
13
14
|
export declare const CLI_FEATURE_USED_TELEMETRY_DISCLOSURE_LINE = "HQ sends a feature-use event when you run a supported CLI feature (feature kind and outcome, plus your company UID when available; never command names, file contents, or secrets). The hq-flags key cli.feature-used-telemetry-v1 controls these events. Set HQ_TELEMETRY=0 to turn this off.";
|
|
15
|
+
export type CliInviteNudgeOutcome = "shown" | "clicked" | "dismissed";
|
|
16
|
+
export interface CliInviteNudgeEvent {
|
|
17
|
+
companyUid: string;
|
|
18
|
+
outcome: CliInviteNudgeOutcome;
|
|
19
|
+
}
|
|
14
20
|
export declare const CLI_LIMIT_AT_ACTION_PROMPT_FLAG = "billing.limit-at-action-prompt";
|
|
15
21
|
export type CliLimitResourceKind = "users" | "integrations" | "secrets" | "agents" | "deployments" | "storageBytes";
|
|
16
22
|
export type CliLimitPresentationKind = "advisory" | "refusal";
|
|
@@ -33,6 +39,11 @@ export declare function takeCliLimitNoticeEngagements(): CliLimitNoticeEngagemen
|
|
|
33
39
|
export declare function queuePlanLimitInviteTeammateExposure(companyUid: string): void;
|
|
34
40
|
/** Drain invite-suggestion exposures for the bounded command-finalization flush. */
|
|
35
41
|
export declare function takePlanLimitInviteTeammateExposures(): string[];
|
|
42
|
+
/** Queue one company-keyed CLI invite prompt outcome; no person UID is included in the event. */
|
|
43
|
+
export declare function queueCliInviteNudgeEvent(companyUid: string, outcome: CliInviteNudgeOutcome): void;
|
|
44
|
+
export declare function takeCliInviteNudgeEvents(): CliInviteNudgeEvent[];
|
|
45
|
+
/** Emit a bounded CLI session fact through the existing product-telemetry path. */
|
|
46
|
+
export declare function emitCliInviteNudgeEvent(event: CliInviteNudgeEvent, timeoutMs?: number): Promise<void>;
|
|
36
47
|
export type CliFeatureKind = "secret_read" | "lane_create" | "lane_resume";
|
|
37
48
|
export type CliFeatureOutcome = "success" | "failure";
|
|
38
49
|
export interface CliFeatureUsage {
|
|
@@ -17,14 +17,17 @@ export const TELEMETRY_TIMEOUT_MS = 1_200;
|
|
|
17
17
|
*/
|
|
18
18
|
export const TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp, never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
|
|
19
19
|
export const FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when you run a signed-in command (CLI version and a timestamp; first-sync guidance shown or followed, with the identifier cli.first-sync-next-step; never command names, file contents, or secrets) to improve the CLI. Set HQ_TELEMETRY=0 to turn this off.";
|
|
20
|
+
export const CLI_INVITE_NUDGE_TELEMETRY_DISCLOSURE_LINE = "HQ sends a usage event when it shows the teammate invite prompt (your company UID and whether you saw, clicked, or dismissed the prompt; no invitee details, command names, file contents, or secrets). Set HQ_TELEMETRY=0 to turn this off.";
|
|
20
21
|
export const CLI_FEATURE_USED_TELEMETRY_FLAG = "cli.feature-used-telemetry-v1";
|
|
21
22
|
export const CLI_FEATURE_USED_TELEMETRY_DISCLOSURE_LINE = "HQ sends a feature-use event when you run a supported CLI feature (feature kind and outcome, plus your company UID when available; never command names, file contents, or secrets). The hq-flags key cli.feature-used-telemetry-v1 controls these events. Set HQ_TELEMETRY=0 to turn this off.";
|
|
22
23
|
const TELEMETRY_DISABLED_VALUES = new Set(["0", "false", "off"]);
|
|
23
24
|
const DISCLOSURE_MARKER = "cli-telemetry-disclosed";
|
|
24
25
|
const FIRST_SYNC_DISCLOSURE_MARKER = "cli-telemetry-disclosed-first-sync-v1";
|
|
26
|
+
const INVITE_NUDGE_DISCLOSURE_MARKER = "cli-telemetry-disclosed-invite-nudge-v1";
|
|
25
27
|
const FEATURE_USED_DISCLOSURE_MARKER = "cli-telemetry-disclosed-feature-used-v1";
|
|
26
28
|
let cliSessionStartedPromise;
|
|
27
29
|
const pendingPlanLimitInviteTeammateExposures = [];
|
|
30
|
+
const pendingCliInviteNudgeEvents = [];
|
|
28
31
|
export const CLI_LIMIT_AT_ACTION_PROMPT_FLAG = "billing.limit-at-action-prompt";
|
|
29
32
|
const pendingCliLimitNoticeExposures = [];
|
|
30
33
|
const pendingCliLimitNoticeEngagements = [];
|
|
@@ -68,6 +71,73 @@ export function queuePlanLimitInviteTeammateExposure(companyUid) {
|
|
|
68
71
|
export function takePlanLimitInviteTeammateExposures() {
|
|
69
72
|
return pendingPlanLimitInviteTeammateExposures.splice(0, pendingPlanLimitInviteTeammateExposures.length);
|
|
70
73
|
}
|
|
74
|
+
/** Queue one company-keyed CLI invite prompt outcome; no person UID is included in the event. */
|
|
75
|
+
export function queueCliInviteNudgeEvent(companyUid, outcome) {
|
|
76
|
+
if (!/^cmp_[A-Za-z0-9_-]{1,128}$/.test(companyUid))
|
|
77
|
+
return;
|
|
78
|
+
pendingCliInviteNudgeEvents.push({ companyUid, outcome });
|
|
79
|
+
}
|
|
80
|
+
export function takeCliInviteNudgeEvents() {
|
|
81
|
+
return pendingCliInviteNudgeEvents.splice(0, pendingCliInviteNudgeEvents.length);
|
|
82
|
+
}
|
|
83
|
+
/** Emit a bounded CLI session fact through the existing product-telemetry path. */
|
|
84
|
+
export async function emitCliInviteNudgeEvent(event, timeoutMs = TELEMETRY_TIMEOUT_MS) {
|
|
85
|
+
try {
|
|
86
|
+
if (isCliTelemetryDisabled())
|
|
87
|
+
return;
|
|
88
|
+
const cached = loadCachedTokens();
|
|
89
|
+
if (!cached || isExpiring(cached, 120) || !cached.accessToken ||
|
|
90
|
+
!isHumanInteractiveCliSession({
|
|
91
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
92
|
+
argv: process.argv,
|
|
93
|
+
env: process.env,
|
|
94
|
+
machineIdentity: isMachineIdentity(),
|
|
95
|
+
}) || process.stdin.isTTY !== true)
|
|
96
|
+
return;
|
|
97
|
+
if (!maybeDiscloseInviteNudgeTelemetry())
|
|
98
|
+
return;
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
const response = await vaultApiFetch({
|
|
103
|
+
token: cached.accessToken,
|
|
104
|
+
path: "/v1/telemetry/events",
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: {
|
|
107
|
+
events: [{
|
|
108
|
+
eventName: "cli_session_started",
|
|
109
|
+
app: "hq-cli",
|
|
110
|
+
source: "cli",
|
|
111
|
+
occurredAt: new Date().toISOString(),
|
|
112
|
+
consentBasis: "disclosed",
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
companyUid: event.companyUid,
|
|
115
|
+
properties: {
|
|
116
|
+
version: CLI_VERSION,
|
|
117
|
+
step: "invite_teammate_nudge",
|
|
118
|
+
outcome: event.outcome,
|
|
119
|
+
},
|
|
120
|
+
}],
|
|
121
|
+
},
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
console.error(`hq: Could not send invite prompt telemetry (http_${response.status}).`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorClass = error instanceof Error ? error.name : "unknown";
|
|
130
|
+
console.error(`hq: Could not send invite prompt telemetry (${errorClass}).`);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const errorClass = error instanceof Error ? error.name : "unknown";
|
|
138
|
+
console.error(`hq: Could not prepare invite prompt telemetry (${errorClass}).`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
71
141
|
const COMPANY_UID_PATTERN = /^cmp_[A-Za-z0-9_-]{1,128}$/;
|
|
72
142
|
export function isCliLimitResourceKind(value) {
|
|
73
143
|
return ["users", "integrations", "secrets", "agents", "deployments", "storageBytes"].includes(value);
|
|
@@ -172,6 +242,9 @@ export function telemetryDisclosureMarkerPath() {
|
|
|
172
242
|
function firstSyncTelemetryDisclosureMarkerPath() {
|
|
173
243
|
return path.join(hqConfigDir(), FIRST_SYNC_DISCLOSURE_MARKER);
|
|
174
244
|
}
|
|
245
|
+
function inviteNudgeTelemetryDisclosureMarkerPath() {
|
|
246
|
+
return path.join(hqConfigDir(), INVITE_NUDGE_DISCLOSURE_MARKER);
|
|
247
|
+
}
|
|
175
248
|
function featureUsedTelemetryDisclosureMarkerPath() {
|
|
176
249
|
return path.join(hqConfigDir(), FEATURE_USED_DISCLOSURE_MARKER);
|
|
177
250
|
}
|
|
@@ -212,6 +285,9 @@ export function maybeDiscloseCliTelemetry(stderr = process.stderr, stdout = proc
|
|
|
212
285
|
function maybeDiscloseFirstSyncTelemetry() {
|
|
213
286
|
return maybeDiscloseTelemetry(FIRST_SYNC_TELEMETRY_DISCLOSURE_LINE, firstSyncTelemetryDisclosureMarkerPath());
|
|
214
287
|
}
|
|
288
|
+
function maybeDiscloseInviteNudgeTelemetry() {
|
|
289
|
+
return maybeDiscloseTelemetry(CLI_INVITE_NUDGE_TELEMETRY_DISCLOSURE_LINE, inviteNudgeTelemetryDisclosureMarkerPath());
|
|
290
|
+
}
|
|
215
291
|
function maybeDiscloseFeatureUsedTelemetry() {
|
|
216
292
|
return maybeDiscloseTelemetry(CLI_FEATURE_USED_TELEMETRY_DISCLOSURE_LINE, featureUsedTelemetryDisclosureMarkerPath());
|
|
217
293
|
}
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Deliberate choices:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - stdio inherits by default. Captured runs pipe only stdout/stderr, and
|
|
12
|
+
* inherit stdin unless the caller supplies an explicit payload. Piping
|
|
13
|
+
* output would buffer streams, reorder stdout against stderr, and require
|
|
14
|
+
* re-emitting bytes, so capture remains an explicit opt-in for native
|
|
15
|
+
* parity fallbacks that need the shell's exact output bytes.
|
|
15
16
|
*
|
|
16
17
|
* - an explicit interpreter (`bash` or `node`) rather than executing the file
|
|
17
18
|
* directly. The executable bit does survive `npm publish` for files that
|
|
@@ -52,6 +53,8 @@ export type RunBundledScriptOptions = {
|
|
|
52
53
|
env?: Record<string, string | undefined>;
|
|
53
54
|
/** Capture stdout and stderr as bytes instead of inheriting those streams. */
|
|
54
55
|
captureOutput?: boolean;
|
|
56
|
+
/** Bytes supplied on stdin. With no value, stdin is inherited as before. */
|
|
57
|
+
stdin?: string | Buffer;
|
|
55
58
|
};
|
|
56
59
|
export type RunBundledScriptResult = {
|
|
57
60
|
/** The child's exit code, or 128+signal if it was terminated by one. */
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Deliberate choices:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - stdio inherits by default. Captured runs pipe only stdout/stderr, and
|
|
12
|
+
* inherit stdin unless the caller supplies an explicit payload. Piping
|
|
13
|
+
* output would buffer streams, reorder stdout against stderr, and require
|
|
14
|
+
* re-emitting bytes, so capture remains an explicit opt-in for native
|
|
15
|
+
* parity fallbacks that need the shell's exact output bytes.
|
|
15
16
|
*
|
|
16
17
|
* - an explicit interpreter (`bash` or `node`) rather than executing the file
|
|
17
18
|
* directly. The executable bit does survive `npm publish` for files that
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
*/
|
|
31
32
|
import { spawnSync } from "child_process";
|
|
32
33
|
import * as fs from "node:fs";
|
|
34
|
+
import * as os from "node:os";
|
|
33
35
|
import * as path from "path";
|
|
34
36
|
import { resolveBundledAsset } from "./hq-roots.js";
|
|
35
37
|
/**
|
|
@@ -152,12 +154,41 @@ export function runBundledScript(opts) {
|
|
|
152
154
|
if (windowsBash && !windowsBash.executable) {
|
|
153
155
|
throw bashNotFoundError(opts.asset, windowsBash.cause);
|
|
154
156
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
let inputDirectory;
|
|
158
|
+
let inputDescriptor;
|
|
159
|
+
let result;
|
|
160
|
+
try {
|
|
161
|
+
if (opts.stdin !== undefined) {
|
|
162
|
+
// Some scaffold scripts reopen /dev/stdin by pathname. Node's spawnSync
|
|
163
|
+
// input pipe cannot be reopened through /dev/stdin on Linux, so capture
|
|
164
|
+
// mode supplies a private regular file descriptor to preserve that ABI.
|
|
165
|
+
const sharedMemory = process.platform === "linux" && fs.existsSync("/dev/shm")
|
|
166
|
+
? "/dev/shm"
|
|
167
|
+
: os.tmpdir();
|
|
168
|
+
inputDirectory = fs.mkdtempSync(path.join(sharedMemory, "hq-bundled-stdin-"));
|
|
169
|
+
const inputPath = path.join(inputDirectory, "stdin");
|
|
170
|
+
fs.writeFileSync(inputPath, opts.stdin, { mode: 0o600 });
|
|
171
|
+
inputDescriptor = fs.openSync(inputPath, "r");
|
|
172
|
+
}
|
|
173
|
+
const stdinStdio = inputDescriptor ?? "inherit";
|
|
174
|
+
const stdio = opts.captureOutput
|
|
175
|
+
? [stdinStdio, "pipe", "pipe"]
|
|
176
|
+
: inputDescriptor !== undefined
|
|
177
|
+
? [stdinStdio, "inherit", "inherit"]
|
|
178
|
+
: "inherit";
|
|
179
|
+
result = spawnSync(executable, [script, ...(opts.args ?? [])], {
|
|
180
|
+
cwd: opts.cwd,
|
|
181
|
+
env,
|
|
182
|
+
stdio,
|
|
183
|
+
...(opts.captureOutput ? { maxBuffer: Number.MAX_SAFE_INTEGER } : {}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
if (inputDescriptor !== undefined)
|
|
188
|
+
fs.closeSync(inputDescriptor);
|
|
189
|
+
if (inputDirectory)
|
|
190
|
+
fs.rmSync(inputDirectory, { recursive: true, force: true });
|
|
191
|
+
}
|
|
161
192
|
if (result.error) {
|
|
162
193
|
const spawnError = result.error;
|
|
163
194
|
if (process.platform === "win32" &&
|
|
@@ -185,8 +216,12 @@ export function runBundledScript(opts) {
|
|
|
185
216
|
return {
|
|
186
217
|
code,
|
|
187
218
|
...(opts.captureOutput ? {
|
|
188
|
-
stdout: result.stdout
|
|
189
|
-
|
|
219
|
+
stdout: Buffer.isBuffer(result.stdout)
|
|
220
|
+
? result.stdout
|
|
221
|
+
: Buffer.from(result.stdout ?? ""),
|
|
222
|
+
stderr: Buffer.isBuffer(result.stderr)
|
|
223
|
+
? result.stderr
|
|
224
|
+
: Buffer.from(result.stderr ?? ""),
|
|
190
225
|
} : {}),
|
|
191
226
|
};
|
|
192
227
|
}
|