@indigoai-us/hq-cli 5.345.160 → 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 +24 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +218 -5
- package/dist/command-catalog.generated.js +269 -5
- package/dist/commands/auth.js +6 -5
- 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/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/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 +11 -0
- package/dist/commands/meetings.js +41 -11
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2486 -1794
- 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/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/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/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/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- 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/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
|
@@ -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
|
|
@@ -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 };
|
|
@@ -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
|
+
}
|