@indigoai-us/hq-cli 5.345.159 → 5.345.161
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 +48 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +222 -9
- package/dist/command-catalog.generated.js +273 -9
- package/dist/commands/access.d.ts +6 -0
- package/dist/commands/access.js +18 -1
- package/dist/commands/acl-lock.d.ts +41 -0
- package/dist/commands/acl-lock.js +28 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +9 -4
- package/dist/commands/agents.js +5 -11
- package/dist/commands/auth.js +6 -5
- package/dist/commands/billing-sponsor.d.ts +4 -4
- package/dist/commands/billing-sponsor.js +11 -11
- package/dist/commands/billing-usage.d.ts +22 -8
- package/dist/commands/billing-usage.js +57 -22
- package/dist/commands/billing.d.ts +7 -7
- package/dist/commands/billing.js +13 -13
- package/dist/commands/cloud-provision.js +2 -2
- package/dist/commands/cloud.js +7 -7
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/db-remote-deps.js +12 -1
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/files.js +19 -7
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/meetings.d.ts +15 -4
- package/dist/commands/meetings.js +58 -19
- package/dist/commands/skill.js +6 -6
- package/dist/commands/whoami.js +1 -1
- package/dist/hook-engine-path.js +13 -2
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3236 -1988
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
- package/dist/lib/billing/plan-limit-refusals.js +26 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
- package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
- package/dist/lib/billing/plan-lock.d.ts +22 -6
- package/dist/lib/billing/plan-lock.js +58 -30
- package/dist/lib/billing/usage-cost.d.ts +24 -2
- package/dist/lib/billing/usage-cost.js +103 -3
- package/dist/lib/billing/usage-only.d.ts +37 -1
- package/dist/lib/billing/usage-only.js +89 -4
- package/dist/lib/billing/usage-pause.d.ts +27 -0
- package/dist/lib/billing/usage-pause.js +50 -0
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +10 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.d.ts +14 -0
- package/dist/lib/hook-engine/resolve.js +55 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/plan-limit-nag.d.ts +0 -16
- package/dist/lib/plan-limit-nag.js +17 -18
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/main.d.ts +0 -2
- package/dist/main.js +19 -57
- package/dist/utils/plan-gate-error.d.ts +30 -14
- package/dist/utils/plan-gate-error.js +60 -46
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/team-upgrade.d.ts +19 -6
- package/dist/utils/team-upgrade.js +37 -39
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { PLAN_LIMIT_DIMENSION_LABELS, renderPersonalPlanFeatureNotice, } from "../lib/plan-limit-nag.js";
|
|
7
7
|
import { withCliLimitEntrySurface } from "../lib/billing/plan-limit-upgrade-url.js";
|
|
8
8
|
import { parseFreeMonth, } from "../lib/billing/free-month.js";
|
|
9
|
-
import { isUsageOnly, pricingPlanFromBody, usageOnlyCardRequiredLines, USAGE_PRICING_POINTER, } from "../lib/billing/usage-only.js";
|
|
10
|
-
/**
|
|
11
|
-
const LEGACY_PLAN_COPY = /Starter|Workforce|Individual|\$500|\$50\b|free month|upgrade/i;
|
|
9
|
+
import { isUsageOnly, pricingPlanFromBody, usageOnlyCardRequiredLines, usagePauseFromBody, USAGE_PRICING_POINTER, } from "../lib/billing/usage-only.js";
|
|
10
|
+
/** Retired plan names, prices and offers a usage-only account must never be shown. */
|
|
11
|
+
const LEGACY_PLAN_COPY = /Starter|Workforce|Individual|\$500|\$50\b|free month|upgrade|plan limit/i;
|
|
12
12
|
/** Copy a plan gate with the caller's pricing plan attached. */
|
|
13
13
|
export function withPricingPlan(err, pricingPlan) {
|
|
14
14
|
if (!pricingPlan || err.details.pricingPlan === pricingPlan)
|
|
@@ -100,23 +100,28 @@ function planLimitRefusalText(head, details) {
|
|
|
100
100
|
`${formatResourceAmount(row.resource, row.used)} of ` +
|
|
101
101
|
`${formatResourceAmount(row.resource, row.limit)} used.`);
|
|
102
102
|
}
|
|
103
|
+
// The free month led to the flat-plan checkout, which is closed; it is
|
|
104
|
+
// never offered. The way out besides trimming is the switch to usage
|
|
105
|
+
// pricing, offered only when the server said the company is on an earlier
|
|
106
|
+
// plan (or that the flat plans are closed to it).
|
|
103
107
|
const fixes = [];
|
|
104
|
-
const freeMonthUrl = safeUpgradeUrl(details.freeMonth?.url);
|
|
105
|
-
if (details.freeMonth?.eligible && freeMonthUrl) {
|
|
106
|
-
fixes.push(`Start your free month — add a card, first month free: ${freeMonthUrl}`);
|
|
107
|
-
}
|
|
108
108
|
for (const [resource, target] of Object.entries(details.fixOptions ?? {})) {
|
|
109
|
-
fixes.push(`${resourceFixAction(resource)} to make room (
|
|
109
|
+
fixes.push(`${resourceFixAction(resource)} to make room (the limit is ` +
|
|
110
110
|
`${formatResourceAmount(resource, target)}).`);
|
|
111
111
|
}
|
|
112
112
|
if (fixes.length > 0) {
|
|
113
|
-
fixes.push(
|
|
113
|
+
fixes.push(details.legacyPlansClosed || details.pricingPlan === "current"
|
|
114
|
+
? "Switch to usage pricing: no plan fee, and these limits stop applying."
|
|
115
|
+
: "Open the company's Billing page.");
|
|
114
116
|
lines.push(" Ways to sort it:");
|
|
115
117
|
fixes.forEach((fix, index) => lines.push(` ${index + 1}. ${fix}`));
|
|
116
118
|
}
|
|
117
119
|
return lines.join("\n");
|
|
118
120
|
}
|
|
119
121
|
function planGateReason(code, details) {
|
|
122
|
+
// The server's usage pause is the whole answer: its message, nothing added.
|
|
123
|
+
if (details.usagePause)
|
|
124
|
+
return details.usagePause.message;
|
|
120
125
|
if (isUsageOnly(details.pricingPlan))
|
|
121
126
|
return usageOnlyPlanGateReason(code, details);
|
|
122
127
|
if (code === "PLAN_LIMIT_EXCEEDED") {
|
|
@@ -125,9 +130,9 @@ function planGateReason(code, details) {
|
|
|
125
130
|
: typeof details.resource === "string" &&
|
|
126
131
|
typeof details.used === "number" &&
|
|
127
132
|
typeof details.limit === "number"
|
|
128
|
-
? `
|
|
133
|
+
? `Limit reached: ${details.resource} ${details.used}/${details.limit} used.`
|
|
129
134
|
: details.resources?.length || details.fixOptions || details.freeMonth
|
|
130
|
-
? "
|
|
135
|
+
? "This account has reached a limit."
|
|
131
136
|
: undefined;
|
|
132
137
|
if (head)
|
|
133
138
|
return planLimitRefusalText(head, details);
|
|
@@ -138,14 +143,19 @@ function planGateReason(code, details) {
|
|
|
138
143
|
return renderPersonalPlanFeatureNotice(details.feature ?? "this feature", null);
|
|
139
144
|
}
|
|
140
145
|
if (code === "MEETING_PLAN_REQUIRED") {
|
|
141
|
-
return "
|
|
146
|
+
return "Meeting notetaker bots are not available on this company's current billing.";
|
|
142
147
|
}
|
|
143
148
|
if (code === "BILLING_REQUIRED") {
|
|
144
|
-
return "
|
|
149
|
+
return "Set up billing for this company before enrolling an external agent.";
|
|
145
150
|
}
|
|
146
|
-
return "
|
|
151
|
+
return "This feature is not available on this company's current billing.";
|
|
147
152
|
}
|
|
148
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* One renderer for server-selected billing links across CLI refusals. The
|
|
155
|
+
* link is labelled `Billing:`, or `Switch:` when the server said it is the
|
|
156
|
+
* usage-pricing switch page. It never says "Upgrade": the flat plans that
|
|
157
|
+
* word led to are closed.
|
|
158
|
+
*/
|
|
149
159
|
export function formatUpgradeRefusal(reason, upgradeUrl, options = {}) {
|
|
150
160
|
const safeUrl = safeUpgradeUrl(upgradeUrl);
|
|
151
161
|
const cliLimitUrl = safeUrl ? withCliLimitEntrySurface(safeUrl) : undefined;
|
|
@@ -153,7 +163,11 @@ export function formatUpgradeRefusal(reason, upgradeUrl, options = {}) {
|
|
|
153
163
|
return cliLimitUrl;
|
|
154
164
|
if (!cliLimitUrl)
|
|
155
165
|
return reason;
|
|
156
|
-
return `${reason}\
|
|
166
|
+
return `${reason}\n${options.legacyPlansClosed ? "Switch" : "Billing"}: ${cliLimitUrl}`;
|
|
167
|
+
}
|
|
168
|
+
/** The link a refusal offers: the usage pause's action, else the server's upgrade URL. */
|
|
169
|
+
export function planGateActionUrl(err) {
|
|
170
|
+
return err.details.usagePause?.actionUrl ?? err.details.upgradeUrl;
|
|
157
171
|
}
|
|
158
172
|
function safeUpgradeUrl(value) {
|
|
159
173
|
if (typeof value !== "string" ||
|
|
@@ -187,6 +201,9 @@ export function companyUidFromPlanUpgradeUrl(value) {
|
|
|
187
201
|
return match?.[1];
|
|
188
202
|
}
|
|
189
203
|
function formatPlanGateErrorText(code, details) {
|
|
204
|
+
if (details.usagePause) {
|
|
205
|
+
return formatUpgradeRefusal(details.usagePause.message, details.usagePause.actionUrl);
|
|
206
|
+
}
|
|
190
207
|
if (isUsageOnly(details.pricingPlan))
|
|
191
208
|
return planGateReason(code, details);
|
|
192
209
|
return formatUpgradeRefusal(planGateReason(code, details), details.upgradeUrl);
|
|
@@ -206,17 +223,21 @@ export function isPlanGateError(err) {
|
|
|
206
223
|
}
|
|
207
224
|
/** Plain, stable CLI copy for plan-gated resource creation. */
|
|
208
225
|
export function formatPlanGateError(err, options = {}) {
|
|
226
|
+
// A usage pause: the server's message and its Billing link, nothing else.
|
|
227
|
+
if (err.details.usagePause) {
|
|
228
|
+
return formatUpgradeRefusal(err.details.usagePause.message, err.details.usagePause.actionUrl, { urlOnly: options.urlOnly });
|
|
229
|
+
}
|
|
209
230
|
// A usage-only account has no plan to upgrade to: no upgrade link.
|
|
210
231
|
if (isUsageOnly(err.details.pricingPlan))
|
|
211
232
|
return planGateReason(err.code, err.details);
|
|
212
|
-
return formatUpgradeRefusal(planGateReason(err.code, err.details), err.details.upgradeUrl, options);
|
|
233
|
+
return formatUpgradeRefusal(planGateReason(err.code, err.details), err.details.upgradeUrl, { ...options, legacyPlansClosed: err.details.legacyPlansClosed === true });
|
|
213
234
|
}
|
|
214
235
|
export function canOfferTeamUpgrade(argv = process.argv) {
|
|
215
236
|
return Boolean(process.stdin.isTTY && process.stderr.isTTY) &&
|
|
216
237
|
!process.env.CI &&
|
|
217
238
|
!argv.includes("--json");
|
|
218
239
|
}
|
|
219
|
-
/** Ask before opening the exact URL returned by the server. */
|
|
240
|
+
/** Ask before opening the exact billing URL returned by the server. */
|
|
220
241
|
export async function offerUpgradeLink(url, deps = {}) {
|
|
221
242
|
const safeUrl = safeUpgradeUrl(url);
|
|
222
243
|
if (!safeUrl)
|
|
@@ -226,13 +247,13 @@ export async function offerUpgradeLink(url, deps = {}) {
|
|
|
226
247
|
return false;
|
|
227
248
|
let answer;
|
|
228
249
|
if (deps.ask) {
|
|
229
|
-
answer = await deps.ask("Open
|
|
250
|
+
answer = await deps.ask("Open the billing link now? [y/N] ");
|
|
230
251
|
}
|
|
231
252
|
else {
|
|
232
253
|
const { createInterface } = await import("node:readline/promises");
|
|
233
254
|
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
234
255
|
try {
|
|
235
|
-
answer = await readline.question("Open
|
|
256
|
+
answer = await readline.question("Open the billing link now? [y/N] ");
|
|
236
257
|
}
|
|
237
258
|
finally {
|
|
238
259
|
readline.close();
|
|
@@ -249,38 +270,29 @@ export async function offerUpgradeLink(url, deps = {}) {
|
|
|
249
270
|
}
|
|
250
271
|
return true;
|
|
251
272
|
}
|
|
252
|
-
/** Legacy checkout callback retained for callers outside the shared refusal paths. */
|
|
253
|
-
export async function offerTeamUpgrade(onConfirm, deps = {}) {
|
|
254
|
-
if (!(deps.interactive ?? canOfferTeamUpgrade(deps.argv)))
|
|
255
|
-
return false;
|
|
256
|
-
if (deps.ask) {
|
|
257
|
-
const answer = await deps.ask("Upgrade now? (opens checkout) [y/N] ");
|
|
258
|
-
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
259
|
-
return false;
|
|
260
|
-
await onConfirm();
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
const { createInterface } = await import("node:readline/promises");
|
|
264
|
-
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
265
|
-
try {
|
|
266
|
-
const answer = await readline.question("Upgrade now? (opens checkout) [y/N] ");
|
|
267
|
-
if (!/^y(?:es)?$/i.test(answer.trim()))
|
|
268
|
-
return false;
|
|
269
|
-
await onConfirm();
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
finally {
|
|
273
|
-
readline.close();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
273
|
/** Decode an already-read JSON envelope from either shared HTTP client. */
|
|
277
274
|
export function planGateErrorFromPayload(status, body, companyUid) {
|
|
278
275
|
if (status !== 402 || !body || typeof body !== "object")
|
|
279
276
|
return null;
|
|
280
277
|
const payload = body;
|
|
281
|
-
const
|
|
278
|
+
const usagePause = usagePauseFromBody(payload);
|
|
279
|
+
// A usage pause in a body this decoder does not otherwise know is still a
|
|
280
|
+
// refusal: the server's message is the whole answer.
|
|
281
|
+
const decoded = decodePlanGatePayload(payload, companyUid) ??
|
|
282
|
+
(usagePause
|
|
283
|
+
? new PlanGateError("PLAN_LIMIT_EXCEEDED", {
|
|
284
|
+
reason: usagePause.message,
|
|
285
|
+
...(usagePause.actionUrl ? { upgradeUrl: usagePause.actionUrl } : {}),
|
|
286
|
+
...(companyUid ? { companyUid } : {}),
|
|
287
|
+
})
|
|
288
|
+
: null);
|
|
289
|
+
if (!decoded)
|
|
290
|
+
return null;
|
|
282
291
|
const pricingPlan = pricingPlanFromBody(payload);
|
|
283
|
-
|
|
292
|
+
const withPlan = pricingPlan ? withPricingPlan(decoded, pricingPlan) : decoded;
|
|
293
|
+
return usagePause
|
|
294
|
+
? new PlanGateError(withPlan.code, { ...withPlan.details, usagePause })
|
|
295
|
+
: withPlan;
|
|
284
296
|
}
|
|
285
297
|
function decodePlanGatePayload(payload, companyUid) {
|
|
286
298
|
// US-054: the personal feature denial. hq-pro identifies it with
|
|
@@ -392,6 +404,7 @@ function planLimitRefusalFromPayload(payload, upgradeUrl, companyUid) {
|
|
|
392
404
|
: undefined;
|
|
393
405
|
const resources = parseResources(payload.resources);
|
|
394
406
|
const fixOptions = parseFixOptions(payload.fixOptions);
|
|
407
|
+
const legacyPlansClosed = payload.legacyPlansClosed === true;
|
|
395
408
|
const parsedFreeMonth = parseFreeMonth(payload.freeMonth);
|
|
396
409
|
const freeMonthUrl = safeUpgradeUrl(parsedFreeMonth?.url);
|
|
397
410
|
const freeMonth = parsedFreeMonth
|
|
@@ -416,6 +429,7 @@ function planLimitRefusalFromPayload(payload, upgradeUrl, companyUid) {
|
|
|
416
429
|
...(upgradeUrl ? { upgradeUrl } : {}),
|
|
417
430
|
...(reason ? { reason } : {}),
|
|
418
431
|
...(companyUid ? { companyUid } : {}),
|
|
432
|
+
...(legacyPlansClosed ? { legacyPlansClosed } : {}),
|
|
419
433
|
});
|
|
420
434
|
}
|
|
421
435
|
/** Wire codes that mark a vault/sync error as a plan-limit refusal. */
|
|
@@ -136,6 +136,8 @@ export declare function acquireUpdateLock(now?: number): (() => void) | null;
|
|
|
136
136
|
/** Injectable surface so the flow is unit-testable without network or spawns. */
|
|
137
137
|
export interface SelfUpdateDeps {
|
|
138
138
|
env?: NodeJS.ProcessEnv;
|
|
139
|
+
/** Platform whose npm launcher layout the update cleans up (tests inject win32). */
|
|
140
|
+
platform?: NodeJS.Platform;
|
|
139
141
|
currentVersion?: string;
|
|
140
142
|
fetchLatest?: () => Promise<string | null>;
|
|
141
143
|
resolveInstall?: () => RunningInstall;
|
|
@@ -70,6 +70,7 @@ import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
|
|
|
70
70
|
import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
|
|
71
71
|
import { markLatestIneffective } from "./version-check.js";
|
|
72
72
|
import { fileSelfUpdateBackoff } from "./self-update-backoff.js";
|
|
73
|
+
import { removeHqCliPs1Shims } from "../lib/windows/ps1-shims.js";
|
|
73
74
|
import { restartDaemonServiceIfInstalled } from "../lib/mesh/live/daemon/install.js";
|
|
74
75
|
import { createCurrentPackageUseLeaseHandoff, PACKAGE_USE_LEASE_HANDOFF_ENV, releaseCurrentPackageUseLease, } from "./package-use-lease.js";
|
|
75
76
|
/**
|
|
@@ -1041,6 +1042,13 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
|
|
|
1041
1042
|
if (!bins.ok)
|
|
1042
1043
|
result = bins;
|
|
1043
1044
|
}
|
|
1045
|
+
// npm regenerates hq.ps1 on every Windows install. PowerShell prefers it to
|
|
1046
|
+
// hq.cmd, and a Restricted execution policy then blocks every hq command.
|
|
1047
|
+
if (result.ok && install.manager === "npm" && install.prefix && (deps.platform ?? process.platform) === "win32") {
|
|
1048
|
+
for (const failure of removeHqCliPs1Shims(install.prefix).failed) {
|
|
1049
|
+
console.error(chalk.yellow(` Could not remove ${failure.file} (${failure.reason}). In PowerShell, run hq.cmd instead of hq.`));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1044
1052
|
if (!result.ok && installSnapshot) {
|
|
1045
1053
|
restoredAfterFailure = restoreInstall(installSnapshot, renamePath);
|
|
1046
1054
|
if (restoredAfterFailure) {
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { isMainModule } from "./is-main-module.js";
|
|
7
|
+
import { removeHqCliPs1Shims } from "../lib/windows/ps1-shims.js";
|
|
7
8
|
const PACKAGE_NAME = "@indigoai-us/hq-cli";
|
|
8
9
|
const RANDOM_HEX_BYTES = 6;
|
|
9
10
|
function packageRoot(prefix, platform) {
|
|
@@ -53,6 +54,13 @@ function temporaryRoot() {
|
|
|
53
54
|
function randomSuffix() {
|
|
54
55
|
return randomBytes(RANDOM_HEX_BYTES).toString("hex");
|
|
55
56
|
}
|
|
57
|
+
/** Best effort: a leftover npm hq .ps1 launcher must never fail an otherwise good update. */
|
|
58
|
+
function removeStalePs1Shims(binDir) {
|
|
59
|
+
const result = removeHqCliPs1Shims(binDir);
|
|
60
|
+
for (const failure of result.failed) {
|
|
61
|
+
console.error("[hq-cli-update] could not remove PowerShell launcher; use hq.cmd", failure);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
56
64
|
function bestEffortRemove(target) {
|
|
57
65
|
if (!target)
|
|
58
66
|
return;
|
|
@@ -149,7 +157,8 @@ export function activateStagedNpmPackage(prefix, stagePrefix, platform = process
|
|
|
149
157
|
continue;
|
|
150
158
|
const executable = path.join(targetPackage, entry);
|
|
151
159
|
if (platform === "win32") {
|
|
152
|
-
|
|
160
|
+
// No .ps1: PowerShell prefers it over hq.cmd and a Restricted policy blocks it.
|
|
161
|
+
for (const extension of ["", ".cmd"]) {
|
|
153
162
|
const stagedFile = path.join(stageBinDir, `${name}${extension}`);
|
|
154
163
|
if (!existsSync(stagedFile))
|
|
155
164
|
continue;
|
|
@@ -178,7 +187,7 @@ export function activateStagedNpmPackage(prefix, stagePrefix, platform = process
|
|
|
178
187
|
renameSync(candidatePackage, targetPackage);
|
|
179
188
|
packageActivated = true;
|
|
180
189
|
for (const name of names) {
|
|
181
|
-
for (const extension of platform === "win32" ? ["", ".cmd"
|
|
190
|
+
for (const extension of platform === "win32" ? ["", ".cmd"] : [""]) {
|
|
182
191
|
const filename = `${name}${extension}`;
|
|
183
192
|
const target = path.join(targetBinDir, filename);
|
|
184
193
|
const staged = path.join(stagedBins, filename);
|
|
@@ -193,6 +202,8 @@ export function activateStagedNpmPackage(prefix, stagePrefix, platform = process
|
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
}
|
|
205
|
+
if (platform === "win32")
|
|
206
|
+
removeStalePs1Shims(targetBinDir);
|
|
196
207
|
bestEffortRemove(packageBackedUp ? backupPackage : undefined);
|
|
197
208
|
bestEffortRemove(backupBins);
|
|
198
209
|
return { ok: true };
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `hq billing upgrade` for a company not on usage pricing (US-024).
|
|
3
|
+
*
|
|
4
|
+
* The flat plans are closed to new subscribers (hq-pro
|
|
5
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19), so there is no
|
|
6
|
+
* checkout to open. The command points at the console's switch to usage
|
|
7
|
+
* pricing for the company. It POSTs nothing: the switch itself, and its owner
|
|
8
|
+
* gate, live on that page.
|
|
9
|
+
*/
|
|
10
|
+
export interface UsageSwitchOptions {
|
|
11
|
+
/** The company slug, for the company's switch page. */
|
|
12
|
+
companySlug?: string;
|
|
4
13
|
noBrowser?: boolean;
|
|
5
14
|
/** Testable override for environments where browser availability is known. */
|
|
6
15
|
headless?: boolean;
|
|
@@ -9,9 +18,13 @@ export interface TeamUpgradeOptions {
|
|
|
9
18
|
write?: (text: string) => void;
|
|
10
19
|
error?: (text: string) => void;
|
|
11
20
|
}
|
|
21
|
+
/** The lines printed before the switch page link. Names no plan. */
|
|
22
|
+
export declare function usageSwitchLines(url: string): string[];
|
|
23
|
+
/** The fallback sentence when no console origin can be derived. */
|
|
24
|
+
export declare const USAGE_SWITCH_NO_URL_MESSAGE = "Switch this company to usage pricing from its Billing page in the HQ Console.";
|
|
12
25
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
26
|
+
* Print (and open, unless told not to) the company's usage-pricing switch
|
|
27
|
+
* page. Returns the URL, or null when none could be built.
|
|
15
28
|
*/
|
|
16
|
-
export declare function
|
|
29
|
+
export declare function openUsageSwitch(opts?: UsageSwitchOptions): Promise<string | null>;
|
|
17
30
|
//# sourceMappingURL=team-upgrade.d.ts.map
|
|
@@ -1,58 +1,56 @@
|
|
|
1
1
|
import { isHeadless } from "./headless.js";
|
|
2
2
|
import open from "open";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { usageSwitchPageUrl } from "../lib/billing/plan-limit-upgrade-url.js";
|
|
4
|
+
import { resolveProcessFlagCompanyContext } from "../lib/process-flag-company-context.js";
|
|
5
|
+
/** The lines printed before the switch page link. Names no plan. */
|
|
6
|
+
export function usageSwitchLines(url) {
|
|
7
|
+
return [
|
|
8
|
+
"The flat plans are closed to new subscribers. To move this company onto usage pricing, switch from its Billing page.",
|
|
9
|
+
"Usage pricing has no plan fee: you pay for what you use.",
|
|
10
|
+
"Add a card first with `hq billing checkout`, then switch here (company owners only):",
|
|
11
|
+
url,
|
|
12
|
+
];
|
|
9
13
|
}
|
|
14
|
+
/** The fallback sentence when no console origin can be derived. */
|
|
15
|
+
export const USAGE_SWITCH_NO_URL_MESSAGE = "Switch this company to usage pricing from its Billing page in the HQ Console.";
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
17
|
+
* Print (and open, unless told not to) the company's usage-pricing switch
|
|
18
|
+
* page. Returns the URL, or null when none could be built.
|
|
13
19
|
*/
|
|
14
|
-
export async function
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
body: {
|
|
22
|
-
companyUid,
|
|
23
|
-
entrySurface: "cli_limit",
|
|
24
|
-
successUrl: CHECKOUT_SUCCESS_URL,
|
|
25
|
-
cancelUrl: CHECKOUT_CANCEL_URL,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
if (!response.ok) {
|
|
29
|
-
const body = (await response.json().catch(() => ({})));
|
|
30
|
-
if (response.status === 403 || /owner/i.test(`${body.error ?? ""} ${body.message ?? ""}`)) {
|
|
31
|
-
throw ownerUpgradeError();
|
|
20
|
+
export async function openUsageSwitch(opts = {}) {
|
|
21
|
+
const write = opts.write ?? ((text) => process.stdout.write(text));
|
|
22
|
+
const slug = opts.companySlug?.trim() || resolveProcessFlagCompanyContext().companySlug || undefined;
|
|
23
|
+
const url = usageSwitchPageUrl(slug);
|
|
24
|
+
if (!url) {
|
|
25
|
+
if (opts.json) {
|
|
26
|
+
write(`${JSON.stringify({ url: null, message: USAGE_SWITCH_NO_URL_MESSAGE })}\n`);
|
|
32
27
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
throw new Error("Checkout response did not include a hosted checkout URL.");
|
|
28
|
+
else {
|
|
29
|
+
write(`${USAGE_SWITCH_NO_URL_MESSAGE}\n`);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
38
32
|
}
|
|
39
|
-
const write = opts.write ?? ((text) => process.stdout.write(text));
|
|
40
33
|
if (opts.json) {
|
|
41
|
-
write(`${JSON.stringify({
|
|
34
|
+
write(`${JSON.stringify({
|
|
35
|
+
url,
|
|
36
|
+
legacyPlansClosed: true,
|
|
37
|
+
message: usageSwitchLines(url).slice(0, 3).join(" "),
|
|
38
|
+
})}\n`);
|
|
42
39
|
return url;
|
|
43
40
|
}
|
|
44
|
-
|
|
41
|
+
for (const line of usageSwitchLines(url))
|
|
42
|
+
write(`${line}\n`);
|
|
45
43
|
const skipBrowser = opts.noBrowser || (opts.headless ?? isHeadless());
|
|
46
|
-
if (skipBrowser)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
if (!skipBrowser)
|
|
45
|
+
await openInBrowser(url, opts);
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
async function openInBrowser(url, opts) {
|
|
50
49
|
try {
|
|
51
50
|
await (opts.openUrl ?? open)(url);
|
|
52
51
|
}
|
|
53
52
|
catch (err) {
|
|
54
53
|
(opts.error ?? ((text) => process.stderr.write(text)))(`Couldn't launch your browser (${err instanceof Error ? err.message : String(err)}). Open this URL to continue: ${url}\n`);
|
|
55
54
|
}
|
|
56
|
-
return url;
|
|
57
55
|
}
|
|
58
56
|
//# sourceMappingURL=team-upgrade.js.map
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
|
|
12
12
|
*
|
|
13
13
|
* The read goes through `/usr/bin/security`, so there is no native module.
|
|
14
|
-
* macOS
|
|
14
|
+
* macOS may ask whether `security` may read the item; HQ accepts
|
|
15
15
|
* that prompt (owner decision) and explains it first:
|
|
16
16
|
* - Before the first read, a one-line note is printed to stderr. It is only
|
|
17
|
-
* shown, and the first read only happens, in an interactive terminal
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* shown, and the first read only happens, in an interactive terminal.
|
|
18
|
+
* A prior success may still have been one-time Allow: it does not prove
|
|
19
|
+
* future reads cannot prompt. After a failure, background reads stop.
|
|
20
|
+
* - A canceled/refused prompt falls back to the file and suppresses reads
|
|
21
|
+
* until `hq auth keychain --retry`. Timeouts and other failures clear
|
|
22
|
+
* remembered access: only a future interactive command can ask again.
|
|
22
23
|
* - "Item not found" is a plain miss and is not remembered as a denial.
|
|
23
24
|
*
|
|
24
25
|
* Choosing between the Keychain and the file (writes still go to the file):
|
|
@@ -38,7 +39,7 @@ export declare const KEYCHAIN_TEAM_ID = "HWFZ2QDTMR";
|
|
|
38
39
|
export declare const KEYCHAIN_READ_TIMEOUT_MS = 3000;
|
|
39
40
|
/** First read: long enough for a person to answer the macOS prompt. */
|
|
40
41
|
export declare const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60000;
|
|
41
|
-
export declare const KEYCHAIN_FIRST_READ_NOTE = "macOS
|
|
42
|
+
export declare const KEYCHAIN_FIRST_READ_NOTE = "macOS may ask whether HQ can read your sign-in from the Keychain. If asked, enter your login keychain password (usually your Mac login password). Choose Always Allow to avoid repeated prompts.";
|
|
42
43
|
export interface KeychainTokenShape {
|
|
43
44
|
accessToken: string;
|
|
44
45
|
idToken: string;
|
|
@@ -55,6 +56,8 @@ export interface KeychainAccessState {
|
|
|
55
56
|
noteShownAt?: string;
|
|
56
57
|
allowedAt?: string;
|
|
57
58
|
deniedAt?: string;
|
|
59
|
+
lastFailureAt?: string;
|
|
60
|
+
failureReason?: "timeout" | "unavailable" | "canceled" | "authorization";
|
|
58
61
|
}
|
|
59
62
|
export interface KeychainReadOptions {
|
|
60
63
|
platform?: NodeJS.Platform;
|
|
@@ -69,7 +72,7 @@ export interface KeychainReadOptions {
|
|
|
69
72
|
retry?: boolean;
|
|
70
73
|
now?: () => Date;
|
|
71
74
|
}
|
|
72
|
-
export type KeychainReadOutcome = "hit" | "miss" | "denied" | "skipped-denied" | "skipped-not-interactive" | "disabled";
|
|
75
|
+
export type KeychainReadOutcome = "hit" | "miss" | "denied" | "skipped-denied" | "skipped-not-interactive" | "unavailable" | "disabled";
|
|
73
76
|
export declare function keychainTokenReadEnabled(opts?: KeychainReadOptions): boolean;
|
|
74
77
|
export declare function keychainAccessStateFile(home?: string): string;
|
|
75
78
|
export declare function readKeychainAccessState(home?: string): KeychainAccessState;
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* - signer: Team ID HWFZ2QDTMR (login-keychain partition `teamid:HWFZ2QDTMR`)
|
|
12
12
|
*
|
|
13
13
|
* The read goes through `/usr/bin/security`, so there is no native module.
|
|
14
|
-
* macOS
|
|
14
|
+
* macOS may ask whether `security` may read the item; HQ accepts
|
|
15
15
|
* that prompt (owner decision) and explains it first:
|
|
16
16
|
* - Before the first read, a one-line note is printed to stderr. It is only
|
|
17
|
-
* shown, and the first read only happens, in an interactive terminal
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* shown, and the first read only happens, in an interactive terminal.
|
|
18
|
+
* A prior success may still have been one-time Allow: it does not prove
|
|
19
|
+
* future reads cannot prompt. After a failure, background reads stop.
|
|
20
|
+
* - A canceled/refused prompt falls back to the file and suppresses reads
|
|
21
|
+
* until `hq auth keychain --retry`. Timeouts and other failures clear
|
|
22
|
+
* remembered access: only a future interactive command can ask again.
|
|
22
23
|
* - "Item not found" is a plain miss and is not remembered as a denial.
|
|
23
24
|
*
|
|
24
25
|
* Choosing between the Keychain and the file (writes still go to the file):
|
|
@@ -44,9 +45,15 @@ export const KEYCHAIN_READ_TIMEOUT_MS = 3_000;
|
|
|
44
45
|
export const KEYCHAIN_FIRST_READ_TIMEOUT_MS = 60_000;
|
|
45
46
|
/** `security` exit status for errSecItemNotFound. */
|
|
46
47
|
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
48
|
+
/** errSecUserCanceled (-128), truncated to the process exit status. This
|
|
49
|
+
* cannot distinguish Deny from Cancel: do not retry either automatically. */
|
|
50
|
+
const SECURITY_USER_CANCELED = 128;
|
|
51
|
+
/** errSecAuthFailed (-25293). A denied authorization and a wrong password
|
|
52
|
+
* cannot be distinguished here, so either requires an explicit retry. */
|
|
53
|
+
const SECURITY_AUTH_FAILED = 51;
|
|
47
54
|
/** Reuse a read within one process for this long (avoids a spawn per request). */
|
|
48
55
|
const READ_CACHE_MS = 30_000;
|
|
49
|
-
export const KEYCHAIN_FIRST_READ_NOTE = "macOS
|
|
56
|
+
export const KEYCHAIN_FIRST_READ_NOTE = "macOS may ask whether HQ can read your sign-in from the Keychain. If asked, enter your login keychain password (usually your Mac login password). Choose Always Allow to avoid repeated prompts.";
|
|
50
57
|
const defaultRunner = (args, timeoutMs) => {
|
|
51
58
|
try {
|
|
52
59
|
const result = spawnSync("/usr/bin/security", args, {
|
|
@@ -144,8 +151,8 @@ export function readKeychainTokensDetailed(opts = {}) {
|
|
|
144
151
|
return { tokens, outcome };
|
|
145
152
|
};
|
|
146
153
|
let state = readKeychainAccessState(home);
|
|
147
|
-
if (opts.retry
|
|
148
|
-
const { deniedAt: _cleared, ...rest } = state;
|
|
154
|
+
if (opts.retry) {
|
|
155
|
+
const { deniedAt: _cleared, allowedAt: _allowed, ...rest } = state;
|
|
149
156
|
state = rest;
|
|
150
157
|
writeKeychainAccessState(home, state);
|
|
151
158
|
}
|
|
@@ -166,15 +173,23 @@ export function readKeychainTokensDetailed(opts = {}) {
|
|
|
166
173
|
const run = opts.runSecurity ?? defaultRunner;
|
|
167
174
|
const result = run(["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], state.allowedAt ? KEYCHAIN_READ_TIMEOUT_MS : KEYCHAIN_FIRST_READ_TIMEOUT_MS);
|
|
168
175
|
if (result.status === 0 && !result.timedOut) {
|
|
169
|
-
|
|
170
|
-
|
|
176
|
+
const { deniedAt: _denied, lastFailureAt: _failed, failureReason: _reason, ...rest } = state;
|
|
177
|
+
writeKeychainAccessState(home, { ...rest, allowedAt: now().toISOString() });
|
|
171
178
|
return remember(parseTokenJson(decodeSecurityOutput(result.stdout)), "hit");
|
|
172
179
|
}
|
|
173
180
|
if (result.status === SECURITY_ITEM_NOT_FOUND && !result.timedOut)
|
|
174
181
|
return remember(null, "miss");
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
// A previous success does not prove Always Allow: another read may prompt.
|
|
183
|
+
// Clear it on failure so a background process cannot prompt on the next run.
|
|
184
|
+
const { allowedAt: _allowed, deniedAt: _denied, ...rest } = state;
|
|
185
|
+
const lastFailureAt = now().toISOString();
|
|
186
|
+
if (!result.timedOut && (result.status === SECURITY_USER_CANCELED || result.status === SECURITY_AUTH_FAILED)) {
|
|
187
|
+
writeKeychainAccessState(home, { ...rest, deniedAt: lastFailureAt, lastFailureAt,
|
|
188
|
+
failureReason: result.status === SECURITY_USER_CANCELED ? "canceled" : "authorization" });
|
|
189
|
+
return remember(null, "denied");
|
|
190
|
+
}
|
|
191
|
+
writeKeychainAccessState(home, { ...rest, lastFailureAt, failureReason: result.timedOut ? "timeout" : "unavailable" });
|
|
192
|
+
return remember(null, "unavailable");
|
|
178
193
|
}
|
|
179
194
|
export function readKeychainTokens(opts = {}) {
|
|
180
195
|
return readKeychainTokensDetailed(opts).tokens;
|
|
@@ -209,10 +224,16 @@ export function readPersonTokensWithKeychain(file = path.join(os.homedir(), ".hq
|
|
|
209
224
|
catch {
|
|
210
225
|
fromFile = null;
|
|
211
226
|
}
|
|
212
|
-
const
|
|
227
|
+
const result = readKeychainTokensDetailed(opts);
|
|
228
|
+
const fromKeychain = result.tokens;
|
|
213
229
|
const chosen = pickNewerSession(fromKeychain, fromFile);
|
|
214
230
|
if (!chosen)
|
|
215
231
|
return { tokens: null, source: "none" };
|
|
232
|
+
if (chosen === fromFile && (result.outcome === "unavailable" || result.outcome === "denied")
|
|
233
|
+
&& (opts.interactive ?? Boolean(process.stderr.isTTY)) && readCache && !readCache.fallbackNotified) {
|
|
234
|
+
(opts.notify ?? ((line) => process.stderr.write(`${line}\n`)))("Keychain access did not complete. HQ used your saved sign-in instead.");
|
|
235
|
+
readCache.fallbackNotified = true;
|
|
236
|
+
}
|
|
216
237
|
return { tokens: chosen, source: chosen === fromKeychain ? "keychain" : "file" };
|
|
217
238
|
}
|
|
218
239
|
//# sourceMappingURL=token-keychain.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.161",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
53
53
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
54
54
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
55
|
-
"@indigoai-us/hq-cloud": "6.18.
|
|
55
|
+
"@indigoai-us/hq-cloud": "6.18.99",
|
|
56
56
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
57
57
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
58
58
|
"@sentry/node": "^10.49.0",
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.161",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.161",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.161",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.161",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.161",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.161"
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
// A source checkout has not built dist during pnpm install. Published tarballs
|
|
3
4
|
// never contain src; a broken tarball warns and keeps existing bash hooks active.
|
|
4
5
|
if (!existsSync(new URL('../dist/lib/hook-engine/resolve.js', import.meta.url)) && existsSync(new URL('../src/index.ts', import.meta.url))) process.exit(0);
|
|
@@ -8,3 +9,14 @@ try {
|
|
|
8
9
|
} catch (error) {
|
|
9
10
|
console.warn(`HQ native hook engine unavailable: ${error instanceof Error ? error.message : String(error)}. Existing bash hooks remain active. Repair: npm install -g @indigoai-us/hq-cli --include=optional`);
|
|
10
11
|
}
|
|
12
|
+
// npm writes hq.ps1 next to hq.cmd on Windows. PowerShell prefers it and the
|
|
13
|
+
// default Restricted execution policy blocks it, so remove hq-cli's own .ps1
|
|
14
|
+
// launchers. Best effort: hq doctor reports any that remain.
|
|
15
|
+
try {
|
|
16
|
+
const { removePs1ShimsAfterNpmInstall } = await import('../dist/lib/windows/ps1-shims.js');
|
|
17
|
+
const packageDir = fileURLToPath(new URL('..', import.meta.url));
|
|
18
|
+
const result = removePs1ShimsAfterNpmInstall(packageDir, { platform: process.platform, global: process.env.npm_config_global === 'true' });
|
|
19
|
+
for (const failure of result?.failed ?? []) console.warn(`Could not remove ${failure.file} (${failure.reason}). In PowerShell, run hq.cmd instead of hq.`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn(`HQ PowerShell launcher cleanup skipped: ${error instanceof Error ? error.message : String(error)}. If PowerShell blocks hq, run hq.cmd doctor --fix.`);
|
|
22
|
+
}
|