@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
package/dist/lib/lanes/store.js
CHANGED
|
@@ -183,7 +183,7 @@ function sameFile(a, b) {
|
|
|
183
183
|
* (another process reclaimed it and took a fresh lock in between), the fresh
|
|
184
184
|
* lock is linked back into place and nothing is reclaimed.
|
|
185
185
|
*/
|
|
186
|
-
function recordLockIsStale(lockPath, deps) {
|
|
186
|
+
function recordLockIsStale(lockPath, deps, reclaimByAge = true) {
|
|
187
187
|
const statSync = deps.statSync ?? ((p) => fs.statSync(p));
|
|
188
188
|
const linkSync = deps.linkSync ?? fs.linkSync.bind(fs);
|
|
189
189
|
const before = statSync(lockPath);
|
|
@@ -199,7 +199,7 @@ function recordLockIsStale(lockPath, deps) {
|
|
|
199
199
|
const mtimeAge = now - before.mtimeMs;
|
|
200
200
|
const age = Number.isFinite(ts) ? Math.max(now - ts, mtimeAge) : mtimeAge;
|
|
201
201
|
const stale = (pidKnown && sameHost && pid !== process.pid && !lockOwnerAlive(pid)) ||
|
|
202
|
-
age > lockStaleMs();
|
|
202
|
+
(reclaimByAge && age > lockStaleMs());
|
|
203
203
|
if (!stale)
|
|
204
204
|
return false;
|
|
205
205
|
const parked = `${lockPath}.stale.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
@@ -243,7 +243,7 @@ function recordLockIsStale(lockPath, deps) {
|
|
|
243
243
|
removeParked();
|
|
244
244
|
return reclaimed;
|
|
245
245
|
}
|
|
246
|
-
function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
246
|
+
function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS, reclaimByAge = true) {
|
|
247
247
|
const lockPath = lockPathFor(recordPath);
|
|
248
248
|
const statSync = deps.statSync ?? ((p) => fs.statSync(p));
|
|
249
249
|
const fstatSync = deps.fstatSync ?? ((fd) => fs.fstatSync(fd));
|
|
@@ -295,7 +295,7 @@ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
295
295
|
throw new LanesError("store_lock_timeout", `Timed out waiting for lock ${lockPath}`, { path: lockPath });
|
|
296
296
|
}
|
|
297
297
|
try {
|
|
298
|
-
if (recordLockIsStale(lockPath, deps))
|
|
298
|
+
if (recordLockIsStale(lockPath, deps, reclaimByAge))
|
|
299
299
|
continue;
|
|
300
300
|
}
|
|
301
301
|
catch (statErr) {
|
|
@@ -309,12 +309,12 @@ function withRecordLock(recordPath, deps, fn, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
/** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
|
|
312
|
-
export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs) {
|
|
313
|
-
return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs);
|
|
312
|
+
export function withRecordLockForTests(recordPath, overrides, fn, timeoutMs, reclaimByAge = true) {
|
|
313
|
+
return withRecordLock(recordPath, { ...defaultStoreDeps(), ...overrides }, fn, timeoutMs, reclaimByAge);
|
|
314
314
|
}
|
|
315
315
|
/** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
|
|
316
|
-
export function withLaneRecordFileLock(recordPath, fn) {
|
|
317
|
-
return withRecordLock(recordPath, defaultStoreDeps(), fn);
|
|
316
|
+
export function withLaneRecordFileLock(recordPath, fn, options = {}) {
|
|
317
|
+
return withRecordLock(recordPath, defaultStoreDeps(), fn, LOCK_TIMEOUT_MS, options.reclaimByAge ?? true);
|
|
318
318
|
}
|
|
319
319
|
function readJsonFile(filePath, deps) {
|
|
320
320
|
let text;
|
|
@@ -79,22 +79,6 @@ export declare function recordPlanLimitStatus(body: unknown, opts?: {
|
|
|
79
79
|
* never claims the workspace has stopped working.
|
|
80
80
|
*/
|
|
81
81
|
export declare function buildOverLine(overEntries: Array<[string, PlanLimitEntry]>, upgradeUrl: string): string;
|
|
82
|
-
/**
|
|
83
|
-
* My Telemetry is an Individual-plan feature on a personal scope (owner
|
|
84
|
-
* decision 9 / us-044-individual-plan-design.md §11). When hq-pro refuses it,
|
|
85
|
-
* the CLI prints this notice INSTEAD of telemetry output.
|
|
86
|
-
*
|
|
87
|
-
* It lives beside the nag copy so plan wording stays in one file, but it is
|
|
88
|
-
* NOT part of the nag model: no persistence, no throttle, no episode state. It
|
|
89
|
-
* is rendered once, on the spot, by whoever received the refusal. The refusal
|
|
90
|
-
* is a plan fact, so it neither counts toward the nag bands nor is silenced by
|
|
91
|
-
* the nag cadence.
|
|
92
|
-
*
|
|
93
|
-
* Copy names the plan and its price only — no Outposts, no bot pricing (owner
|
|
94
|
-
* decisions 10 and 11).
|
|
95
|
-
*/
|
|
96
|
-
export declare const INDIVIDUAL_PLAN_NAME = "Individual";
|
|
97
|
-
export declare const INDIVIDUAL_PRICE_LABEL = "$50/mo";
|
|
98
82
|
/**
|
|
99
83
|
* The notice a personal feature refusal prints. `upgradeUrl` is the server's
|
|
100
84
|
* own checkout target; an older hq-pro sends none, and the line then names the
|
|
@@ -367,16 +367,18 @@ export function buildOverLine(overEntries, upgradeUrl) {
|
|
|
367
367
|
const facts = overEntries
|
|
368
368
|
.map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${entry.used} of ${entry.limit}`)
|
|
369
369
|
.join(", ");
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
// US-024: names no plan and offers no upgrade; the link is the Billing page.
|
|
371
|
+
return (`⚠ Over this account's limits: ${facts}. New files and new secrets are paused. ` +
|
|
372
|
+
`Billing: ${upgradeUrl}`);
|
|
372
373
|
}
|
|
373
374
|
// ---------------------------------------------------------------------------
|
|
374
|
-
//
|
|
375
|
+
// Personal feature notice (US-054, copy per US-024)
|
|
375
376
|
// ---------------------------------------------------------------------------
|
|
376
377
|
/**
|
|
377
|
-
* My Telemetry is
|
|
378
|
-
*
|
|
379
|
-
*
|
|
378
|
+
* My Telemetry is a paid feature on a personal scope (owner decision 9 /
|
|
379
|
+
* us-044-individual-plan-design.md §11). When hq-pro refuses it, the CLI
|
|
380
|
+
* prints this notice INSTEAD of telemetry output. The flat personal plan is
|
|
381
|
+
* closed (US-024), so the notice names no plan or price.
|
|
380
382
|
*
|
|
381
383
|
* It lives beside the nag copy so plan wording stays in one file, but it is
|
|
382
384
|
* NOT part of the nag model: no persistence, no throttle, no episode state. It
|
|
@@ -384,11 +386,8 @@ export function buildOverLine(overEntries, upgradeUrl) {
|
|
|
384
386
|
* is a plan fact, so it neither counts toward the nag bands nor is silenced by
|
|
385
387
|
* the nag cadence.
|
|
386
388
|
*
|
|
387
|
-
*
|
|
388
|
-
* decisions 10 and 11).
|
|
389
|
+
* No Outposts and no bot pricing in the copy (owner decisions 10 and 11).
|
|
389
390
|
*/
|
|
390
|
-
export const INDIVIDUAL_PLAN_NAME = "Individual";
|
|
391
|
-
export const INDIVIDUAL_PRICE_LABEL = "$50/mo";
|
|
392
391
|
/** Human names for the features a personal scope can be refused. */
|
|
393
392
|
const PERSONAL_FEATURE_LABELS = {
|
|
394
393
|
telemetry: "My Telemetry",
|
|
@@ -403,11 +402,11 @@ export function renderPersonalPlanFeatureNotice(feature, upgradeUrl, opts = {})
|
|
|
403
402
|
// A person on usage pricing has no Individual plan to buy (US-012).
|
|
404
403
|
if (isUsageOnly(opts.pricingPlan))
|
|
405
404
|
return usageOnlyPersonalFeatureNotice(label);
|
|
406
|
-
const headline = `${label} is included
|
|
405
|
+
const headline = `${label} is not included on your personal account's current billing.`;
|
|
407
406
|
const action = typeof upgradeUrl === "string" && upgradeUrl.trim().length > 0
|
|
408
|
-
? `
|
|
409
|
-
: "
|
|
410
|
-
return `${headline}
|
|
407
|
+
? `Billing: ${upgradeUrl.trim()}`
|
|
408
|
+
: "See your HQ billing settings.";
|
|
409
|
+
return `${headline} ${action}`;
|
|
411
410
|
}
|
|
412
411
|
/** `"{resource}:{band}"` — the persisted key, matching the server's own. */
|
|
413
412
|
function crossingKey(crossing) {
|
|
@@ -429,9 +428,9 @@ export function renderThresholdNotice(crossing, upgradeUrl) {
|
|
|
429
428
|
const name = dimensionName(crossing.resource);
|
|
430
429
|
const counts = `${crossing.used} of ${crossing.limit}`;
|
|
431
430
|
const headline = crossing.band === "over"
|
|
432
|
-
? `
|
|
433
|
-
: `
|
|
434
|
-
return `${headline}
|
|
431
|
+
? `You are past your ${name} limit (${counts}).`
|
|
432
|
+
: `You have reached 80% of your ${name} limit (${counts}).`;
|
|
433
|
+
return `${headline} Billing: ${upgradeUrl}`;
|
|
435
434
|
}
|
|
436
435
|
/**
|
|
437
436
|
* Print every crossing this machine has not already printed for this episode.
|
|
@@ -521,7 +520,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
521
520
|
if (worst === null)
|
|
522
521
|
return;
|
|
523
522
|
warningShownThisSession = true;
|
|
524
|
-
const line = `⚠
|
|
523
|
+
const line = `⚠ Nearing this account's limits: ${formatEntryLine(worst.key, worst.entry)}. Make room before you hit the cap. Billing: ${resolvedUpgradeUrl}`;
|
|
525
524
|
write(chalk.yellow(line) + "\n");
|
|
526
525
|
if (isCliLimitResourceKind(worst.key)) {
|
|
527
526
|
queueCliLimitNoticeExposure(worst.key, "advisory");
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** hq-cli's own bins and the package file each npm launcher runs. */
|
|
2
|
+
export declare const HQ_CLI_BINS: Readonly<Record<string, string>>;
|
|
3
|
+
/**
|
|
4
|
+
* True when `content` is npm's (cmd-shim) PowerShell launcher for the hq-cli
|
|
5
|
+
* bin `bin`. The launcher resolves its own folder, runs node on
|
|
6
|
+
* `$basedir/node_modules/@indigoai-us/hq-cli/<entry>` and exits with node's
|
|
7
|
+
* code. Either slash direction is accepted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isNpmGeneratedHqCliPs1Shim(content: string, bin: string): boolean;
|
|
10
|
+
export interface Ps1ShimFs {
|
|
11
|
+
readFile: (file: string) => string;
|
|
12
|
+
remove: (file: string) => void;
|
|
13
|
+
}
|
|
14
|
+
/** The npm-generated hq-cli `.ps1` launchers present in `binDir`. */
|
|
15
|
+
export declare function findHqCliPs1Shims(binDir: string, io?: Ps1ShimFs): string[];
|
|
16
|
+
export interface RemovePs1ShimsResult {
|
|
17
|
+
/** Launchers removed by this call. */
|
|
18
|
+
removed: string[];
|
|
19
|
+
/** `.ps1` files with an hq-cli bin name that are not npm's launcher. */
|
|
20
|
+
kept: string[];
|
|
21
|
+
/** Launchers that could not be removed, with the reason. */
|
|
22
|
+
failed: Array<{
|
|
23
|
+
file: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove the npm-generated `.ps1` launchers for hq-cli's bins from `binDir`
|
|
29
|
+
* (the npm global prefix on Windows). Safe to run again: a missing file is
|
|
30
|
+
* not an error. Never touches another package's `.ps1` or a hand-written one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function removeHqCliPs1Shims(binDir: string, io?: Ps1ShimFs): RemovePs1ShimsResult;
|
|
33
|
+
/**
|
|
34
|
+
* True when the effective execution policy blocks npm's unsigned launcher.
|
|
35
|
+
* `Undefined` as the effective policy means no scope set one, which Windows
|
|
36
|
+
* client editions treat as `Restricted`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function policyBlocksUnsignedScripts(policy: string): boolean;
|
|
39
|
+
export type ExecutionPolicyRun = () => {
|
|
40
|
+
status: number | null;
|
|
41
|
+
stdout: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Read the current user's effective PowerShell execution policy. Read-only:
|
|
45
|
+
* it runs `Get-ExecutionPolicy` and never sets anything. Null when
|
|
46
|
+
* PowerShell could not be run or printed nothing usable.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readExecutionPolicy(run?: ExecutionPolicyRun): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Postinstall entry: after a global npm install on Windows, remove hq-cli's
|
|
51
|
+
* `.ps1` launchers from the prefix holding `packageDir`. Acts only when the
|
|
52
|
+
* package sits at `<prefix>/node_modules/@indigoai-us/hq-cli`, npm's Windows
|
|
53
|
+
* global layout. Null when it did nothing. npm 11.16+ may skip lifecycle
|
|
54
|
+
* scripts, so `hq doctor` reports any launcher left behind.
|
|
55
|
+
*/
|
|
56
|
+
export declare function removePs1ShimsAfterNpmInstall(packageDir: string, opts: {
|
|
57
|
+
platform: NodeJS.Platform;
|
|
58
|
+
global: boolean;
|
|
59
|
+
}, io?: Ps1ShimFs): RemovePs1ShimsResult | null;
|
|
60
|
+
//# sourceMappingURL=ps1-shims.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm writes three launchers for every global bin on Windows: `hq`, `hq.cmd`
|
|
3
|
+
* and `hq.ps1`. PowerShell prefers `hq.ps1`, and under the default
|
|
4
|
+
* `Restricted` execution policy it refuses to run it ("running scripts is
|
|
5
|
+
* disabled on this system", UnauthorizedAccess). `hq.cmd` works under every
|
|
6
|
+
* policy, so hq-cli removes its own `.ps1` launchers and PowerShell falls
|
|
7
|
+
* through to `hq.cmd`.
|
|
8
|
+
*
|
|
9
|
+
* Only a file whose content is npm's generated launcher for an hq-cli bin is
|
|
10
|
+
* removed. A `.ps1` written by a person or another package is left alone. The
|
|
11
|
+
* execution policy is read only, never changed.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
/** hq-cli's own bins and the package file each npm launcher runs. */
|
|
17
|
+
export const HQ_CLI_BINS = {
|
|
18
|
+
hq: "dist/index.js",
|
|
19
|
+
"hq-auth-refresh": "dist/bin/hq-auth-refresh.js",
|
|
20
|
+
};
|
|
21
|
+
const PACKAGE_PATH = "node_modules/@indigoai-us/hq-cli/";
|
|
22
|
+
/**
|
|
23
|
+
* True when `content` is npm's (cmd-shim) PowerShell launcher for the hq-cli
|
|
24
|
+
* bin `bin`. The launcher resolves its own folder, runs node on
|
|
25
|
+
* `$basedir/node_modules/@indigoai-us/hq-cli/<entry>` and exits with node's
|
|
26
|
+
* code. Either slash direction is accepted.
|
|
27
|
+
*/
|
|
28
|
+
export function isNpmGeneratedHqCliPs1Shim(content, bin) {
|
|
29
|
+
const entry = HQ_CLI_BINS[bin];
|
|
30
|
+
if (!entry)
|
|
31
|
+
return false;
|
|
32
|
+
const text = content.replace(/\r\n/g, "\n").replace(/\\/g, "/");
|
|
33
|
+
if (!text.startsWith("#!/usr/bin/env pwsh\n"))
|
|
34
|
+
return false;
|
|
35
|
+
if (!text.includes("$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent"))
|
|
36
|
+
return false;
|
|
37
|
+
if (!text.trimEnd().endsWith("exit $ret"))
|
|
38
|
+
return false;
|
|
39
|
+
return text.includes(`"$basedir/${PACKAGE_PATH}${entry}"`);
|
|
40
|
+
}
|
|
41
|
+
const realFs = {
|
|
42
|
+
readFile: (file) => fs.readFileSync(file, "utf8"),
|
|
43
|
+
remove: (file) => fs.rmSync(file, { force: true }),
|
|
44
|
+
};
|
|
45
|
+
function isMissing(error) {
|
|
46
|
+
const code = error?.code;
|
|
47
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
48
|
+
}
|
|
49
|
+
/** The npm-generated hq-cli `.ps1` launchers present in `binDir`. */
|
|
50
|
+
export function findHqCliPs1Shims(binDir, io = realFs) {
|
|
51
|
+
const found = [];
|
|
52
|
+
for (const bin of Object.keys(HQ_CLI_BINS)) {
|
|
53
|
+
const file = path.join(binDir, `${bin}.ps1`);
|
|
54
|
+
let content;
|
|
55
|
+
try {
|
|
56
|
+
content = io.readFile(file);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (isNpmGeneratedHqCliPs1Shim(content, bin))
|
|
62
|
+
found.push(file);
|
|
63
|
+
}
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove the npm-generated `.ps1` launchers for hq-cli's bins from `binDir`
|
|
68
|
+
* (the npm global prefix on Windows). Safe to run again: a missing file is
|
|
69
|
+
* not an error. Never touches another package's `.ps1` or a hand-written one.
|
|
70
|
+
*/
|
|
71
|
+
export function removeHqCliPs1Shims(binDir, io = realFs) {
|
|
72
|
+
const result = { removed: [], kept: [], failed: [] };
|
|
73
|
+
for (const bin of Object.keys(HQ_CLI_BINS)) {
|
|
74
|
+
const file = path.join(binDir, `${bin}.ps1`);
|
|
75
|
+
let content;
|
|
76
|
+
try {
|
|
77
|
+
content = io.readFile(file);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!isMissing(error)) {
|
|
81
|
+
result.failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!isNpmGeneratedHqCliPs1Shim(content, bin)) {
|
|
86
|
+
result.kept.push(file);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
io.remove(file);
|
|
91
|
+
result.removed.push(file);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (isMissing(error))
|
|
95
|
+
continue;
|
|
96
|
+
result.failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
/** Policies under which PowerShell refuses to run an unsigned local `.ps1`. */
|
|
102
|
+
const BLOCKING_POLICIES = new Set(["restricted", "allsigned", "undefined"]);
|
|
103
|
+
/**
|
|
104
|
+
* True when the effective execution policy blocks npm's unsigned launcher.
|
|
105
|
+
* `Undefined` as the effective policy means no scope set one, which Windows
|
|
106
|
+
* client editions treat as `Restricted`.
|
|
107
|
+
*/
|
|
108
|
+
export function policyBlocksUnsignedScripts(policy) {
|
|
109
|
+
return BLOCKING_POLICIES.has(policy.trim().toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
function defaultPolicyRun() {
|
|
112
|
+
const run = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "Get-ExecutionPolicy"], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
113
|
+
return { status: run.status, stdout: typeof run.stdout === "string" ? run.stdout : "" };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read the current user's effective PowerShell execution policy. Read-only:
|
|
117
|
+
* it runs `Get-ExecutionPolicy` and never sets anything. Null when
|
|
118
|
+
* PowerShell could not be run or printed nothing usable.
|
|
119
|
+
*/
|
|
120
|
+
export function readExecutionPolicy(run = defaultPolicyRun) {
|
|
121
|
+
try {
|
|
122
|
+
const { status, stdout } = run();
|
|
123
|
+
if (status !== 0)
|
|
124
|
+
return null;
|
|
125
|
+
const policy = stdout.trim().split(/\s+/).pop() ?? "";
|
|
126
|
+
return /^[A-Za-z]+$/.test(policy) ? policy : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Postinstall entry: after a global npm install on Windows, remove hq-cli's
|
|
134
|
+
* `.ps1` launchers from the prefix holding `packageDir`. Acts only when the
|
|
135
|
+
* package sits at `<prefix>/node_modules/@indigoai-us/hq-cli`, npm's Windows
|
|
136
|
+
* global layout. Null when it did nothing. npm 11.16+ may skip lifecycle
|
|
137
|
+
* scripts, so `hq doctor` reports any launcher left behind.
|
|
138
|
+
*/
|
|
139
|
+
export function removePs1ShimsAfterNpmInstall(packageDir, opts, io = realFs) {
|
|
140
|
+
if (opts.platform !== "win32" || !opts.global)
|
|
141
|
+
return null;
|
|
142
|
+
const dir = path.resolve(packageDir);
|
|
143
|
+
const scope = path.dirname(dir);
|
|
144
|
+
const nodeModules = path.dirname(scope);
|
|
145
|
+
if (path.basename(dir).toLowerCase() !== "hq-cli" ||
|
|
146
|
+
path.basename(scope).toLowerCase() !== "@indigoai-us" ||
|
|
147
|
+
path.basename(nodeModules).toLowerCase() !== "node_modules") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return removeHqCliPs1Shims(path.dirname(nodeModules), io);
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=ps1-shims.js.map
|
package/dist/main.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ export { handleStreamError } from "./stream-error-boundary.js";
|
|
|
9
9
|
export type { StreamErrorDependencies } from "./stream-error-boundary.js";
|
|
10
10
|
import { type PlanGateError } from "./utils/plan-gate-error.js";
|
|
11
11
|
import { type PricingPlan } from "./lib/billing/usage-only.js";
|
|
12
|
-
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
13
12
|
/**
|
|
14
13
|
* Total time one CLI invocation may await foreground network work. This spans
|
|
15
14
|
* the hard version decision, pre-action observability, and release-health
|
|
@@ -44,7 +43,6 @@ export type TopLevelErrorDependencies = {
|
|
|
44
43
|
argv?: readonly string[];
|
|
45
44
|
ask?: (question: string) => Promise<string>;
|
|
46
45
|
openUrl?: (url: string) => Promise<void>;
|
|
47
|
-
upgradeToTeam?: typeof upgradeToTeam;
|
|
48
46
|
/**
|
|
49
47
|
* Usage pricing (US-012): resolve the account's pricing plan for a plan gate
|
|
50
48
|
* whose 402 body did not carry one. Absent = use the body only.
|
package/dist/main.js
CHANGED
|
@@ -40,9 +40,8 @@ import { browserLoginAbandonedMessage } from "./utils/browser-login-abandoned.js
|
|
|
40
40
|
import { callbackPortBusyMessage, DEFAULT_CALLBACK_PORT } from "./utils/callback-port-busy.js";
|
|
41
41
|
import { DEFAULT_COGNITO, isMachineIdentity, loadMachineCachedTokens, } from "./utils/cognito-session.js";
|
|
42
42
|
import { isCompanySelectionError } from "./utils/company-selection-error.js";
|
|
43
|
-
import { canOfferTeamUpgrade, formatPlanGateError,
|
|
43
|
+
import { canOfferTeamUpgrade, formatPlanGateError, offerUpgradeLink, planGateActionUrl, planGateErrorFromVaultError, withPricingPlan, } from "./utils/plan-gate-error.js";
|
|
44
44
|
import { isUsageOnly, resolveCompanyPricingPlan, resolvePersonPricingPlan, } from "./lib/billing/usage-only.js";
|
|
45
|
-
import { upgradeToTeam } from "./utils/team-upgrade.js";
|
|
46
45
|
import { printCachedVersionNudge, printVersionAndNudge, refreshVersionCache, staleAgainstCachedLatest, } from "./utils/version-check.js";
|
|
47
46
|
import { enforceVersionGate, getVersionGatePhase, isLocalDependencyInstall, resolveRunningInstall, shouldSkipGate, } from "./utils/version-gate.js";
|
|
48
47
|
import { autoUpdateAndReexec } from "./utils/self-update.js";
|
|
@@ -654,8 +653,8 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
654
653
|
}
|
|
655
654
|
else if (planGate) {
|
|
656
655
|
// Plan denials are expected product limits, never a CLI crash. The
|
|
657
|
-
// server URL is always shown.
|
|
658
|
-
//
|
|
656
|
+
// server URL is always shown. The flat-plan checkout is closed, so the
|
|
657
|
+
// only offer is to open the server's billing link.
|
|
659
658
|
//
|
|
660
659
|
// A plan-limit refusal (a Starter hard stop) always prints its reason,
|
|
661
660
|
// usage and fix options, even non-interactively: the user can fix it
|
|
@@ -671,60 +670,23 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
|
|
|
671
670
|
? queueCliLimitNoticeExposure(planGate.details.resource, "refusal", planGate.details.companyUid)
|
|
672
671
|
: null;
|
|
673
672
|
deps.setExitCode(1);
|
|
674
|
-
// A usage-only account has no plan to buy: no
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
interactive: true,
|
|
688
|
-
argv: deps.argv,
|
|
689
|
-
ask: deps.ask,
|
|
690
|
-
});
|
|
691
|
-
if (upgraded)
|
|
692
|
-
queueCliLimitNoticeEngagement(promptExposure);
|
|
693
|
-
}
|
|
694
|
-
catch (upgradeErr) {
|
|
695
|
-
const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
|
|
696
|
-
deps.stderr.write(`hq: ${message}\n`);
|
|
697
|
-
try {
|
|
698
|
-
const opened = await offerUpgradeLink(planGate.details.upgradeUrl, {
|
|
699
|
-
interactive: true,
|
|
700
|
-
argv: deps.argv,
|
|
701
|
-
ask: deps.ask,
|
|
702
|
-
openUrl: deps.openUrl,
|
|
703
|
-
});
|
|
704
|
-
if (opened)
|
|
705
|
-
queueCliLimitNoticeEngagement(promptExposure);
|
|
706
|
-
}
|
|
707
|
-
catch (linkErr) {
|
|
708
|
-
const linkMessage = linkErr instanceof Error ? linkErr.message : String(linkErr);
|
|
709
|
-
deps.stderr.write(`hq: ${linkMessage}\n`);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
673
|
+
// A usage-only account has no plan to buy: no upgrade offer. A usage
|
|
674
|
+
// pause offers its own Billing link.
|
|
675
|
+
const actionUrl = planGateActionUrl(planGate);
|
|
676
|
+
if (interactive && actionUrl && (!usageOnlyGate || planGate.details.usagePause)) {
|
|
677
|
+
try {
|
|
678
|
+
const opened = await offerUpgradeLink(actionUrl, {
|
|
679
|
+
interactive: true,
|
|
680
|
+
argv: deps.argv,
|
|
681
|
+
ask: deps.ask,
|
|
682
|
+
openUrl: deps.openUrl,
|
|
683
|
+
});
|
|
684
|
+
if (opened)
|
|
685
|
+
queueCliLimitNoticeEngagement(promptExposure);
|
|
712
686
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
interactive: true,
|
|
717
|
-
argv: deps.argv,
|
|
718
|
-
ask: deps.ask,
|
|
719
|
-
openUrl: deps.openUrl,
|
|
720
|
-
});
|
|
721
|
-
if (opened)
|
|
722
|
-
queueCliLimitNoticeEngagement(promptExposure);
|
|
723
|
-
}
|
|
724
|
-
catch (upgradeErr) {
|
|
725
|
-
const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
|
|
726
|
-
deps.stderr.write(`hq: ${message}\n`);
|
|
727
|
-
}
|
|
687
|
+
catch (upgradeErr) {
|
|
688
|
+
const message = upgradeErr instanceof Error ? upgradeErr.message : String(upgradeErr);
|
|
689
|
+
deps.stderr.write(`hq: ${message}\n`);
|
|
728
690
|
}
|
|
729
691
|
}
|
|
730
692
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type PlanLimitFreeMonth } from "../lib/billing/free-month.js";
|
|
2
|
-
import { type PricingPlan } from "../lib/billing/usage-only.js";
|
|
2
|
+
import { type PricingPlan, type UsagePause } from "../lib/billing/usage-only.js";
|
|
3
3
|
export type PlanGateCode = "PLAN_LIMIT_EXCEEDED" | "PLAN_REQUIRED" | "MEETING_PLAN_REQUIRED"
|
|
4
|
-
/** A PERSONAL scope asked for a feature
|
|
4
|
+
/** A PERSONAL scope asked for a feature its current plan does not include. */
|
|
5
5
|
| "PERSONAL_PLAN_REQUIRED"
|
|
6
6
|
/** External agent enrollment was denied by the company plan. */
|
|
7
7
|
| "BILLING_REQUIRED";
|
|
@@ -29,19 +29,41 @@ export interface PlanGateDetails {
|
|
|
29
29
|
/** Hard-stop 402: the free-month offer, present only when it stands. */
|
|
30
30
|
freeMonth?: PlanLimitFreeMonth;
|
|
31
31
|
/**
|
|
32
|
-
* Usage pricing (US-012): from the 402 body
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* upgrade link. Absent
|
|
32
|
+
* Usage pricing (US-012): from the 402 body (`billingState` first, then the
|
|
33
|
+
* legacy `pricingPlan`), or attached by the caller from the membership row,
|
|
34
|
+
* usage-limits or the billing summary. `"usage-only"` renders no retired
|
|
35
|
+
* plan copy and no plan upgrade link. Absent (unknown) renders neutral copy
|
|
36
|
+
* that names no plan.
|
|
36
37
|
*/
|
|
37
38
|
pricingPlan?: PricingPlan;
|
|
39
|
+
/**
|
|
40
|
+
* The server's usage pause (`usagePause` on the 402 body, hq-pro
|
|
41
|
+
* src/billing/usage-pause.ts). When present its message is the whole
|
|
42
|
+
* refusal and its `actionUrl` the only link.
|
|
43
|
+
*/
|
|
44
|
+
usagePause?: UsagePause;
|
|
45
|
+
/**
|
|
46
|
+
* Hard-stop 402: true when HQ Workforce is closed to the company (hq-pro
|
|
47
|
+
* `billing.legacy-plans-closed`, owner decision 2026-10-19). `upgradeUrl`
|
|
48
|
+
* is then the usage-pricing switch page; the refusal offers the switch and
|
|
49
|
+
* never the free Workforce month.
|
|
50
|
+
*/
|
|
51
|
+
legacyPlansClosed?: boolean;
|
|
38
52
|
}
|
|
39
53
|
/** Copy a plan gate with the caller's pricing plan attached. */
|
|
40
54
|
export declare function withPricingPlan(err: PlanGateError, pricingPlan: PricingPlan | null | undefined): PlanGateError;
|
|
41
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* One renderer for server-selected billing links across CLI refusals. The
|
|
57
|
+
* link is labelled `Billing:`, or `Switch:` when the server said it is the
|
|
58
|
+
* usage-pricing switch page. It never says "Upgrade": the flat plans that
|
|
59
|
+
* word led to are closed.
|
|
60
|
+
*/
|
|
42
61
|
export declare function formatUpgradeRefusal(reason: string, upgradeUrl: string | undefined, options?: {
|
|
43
62
|
urlOnly?: boolean;
|
|
63
|
+
legacyPlansClosed?: boolean;
|
|
44
64
|
}): string;
|
|
65
|
+
/** The link a refusal offers: the usage pause's action, else the server's upgrade URL. */
|
|
66
|
+
export declare function planGateActionUrl(err: PlanGateError): string | undefined;
|
|
45
67
|
/** Recover a company UID only from the server's UID-keyed billing callback. */
|
|
46
68
|
export declare function companyUidFromPlanUpgradeUrl(value: unknown): string | undefined;
|
|
47
69
|
export declare class PlanGateError extends Error {
|
|
@@ -61,14 +83,8 @@ export interface UpgradeLinkOfferDependencies {
|
|
|
61
83
|
argv?: readonly string[];
|
|
62
84
|
interactive?: boolean;
|
|
63
85
|
}
|
|
64
|
-
/** Ask before opening the exact URL returned by the server. */
|
|
86
|
+
/** Ask before opening the exact billing URL returned by the server. */
|
|
65
87
|
export declare function offerUpgradeLink(url: string, deps?: UpgradeLinkOfferDependencies): Promise<boolean>;
|
|
66
|
-
/** Legacy checkout callback retained for callers outside the shared refusal paths. */
|
|
67
|
-
export declare function offerTeamUpgrade(onConfirm: () => Promise<void>, deps?: {
|
|
68
|
-
ask?: (question: string) => Promise<string>;
|
|
69
|
-
argv?: readonly string[];
|
|
70
|
-
interactive?: boolean;
|
|
71
|
-
}): Promise<boolean>;
|
|
72
88
|
/** Decode an already-read JSON envelope from either shared HTTP client. */
|
|
73
89
|
export declare function planGateErrorFromPayload(status: number, body: unknown, companyUid?: string): PlanGateError | null;
|
|
74
90
|
/**
|