@indigoai-us/hq-cli 5.345.148 → 5.345.149
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 +35 -0
- package/README.md +2 -0
- package/dist/command-catalog.generated.d.ts +44 -8
- package/dist/command-catalog.generated.js +53 -8
- package/dist/command-help.js +2 -1
- package/dist/commands/access.d.ts +45 -2
- package/dist/commands/access.js +537 -223
- package/dist/commands/billing-usage.d.ts +33 -2
- package/dist/commands/billing-usage.js +75 -4
- package/dist/commands/bot.js +2 -2
- package/dist/commands/channels.d.ts +27 -0
- package/dist/commands/channels.js +108 -4
- package/dist/commands/core-refresh-vault-access-native.d.ts +16 -0
- package/dist/commands/core-refresh-vault-access-native.js +142 -9
- package/dist/commands/core.js +4 -1
- package/dist/commands/dm.js +9 -1
- package/dist/commands/install-global.d.ts +1 -8
- package/dist/commands/install-global.js +11 -27
- package/dist/commands/library-add.d.ts +31 -1
- package/dist/commands/library-add.js +69 -2
- package/dist/commands/library.js +37 -3
- package/dist/commands/reindex.js +19 -0
- package/dist/hook-engine-path.js +10 -3
- package/dist/index.d.ts +10 -0
- package/dist/index.js +70 -5
- package/dist/lazy-commands.js +2 -0
- package/dist/lib/billing/usage-cost.d.ts +31 -1
- package/dist/lib/billing/usage-cost.js +46 -0
- package/dist/lib/db/local.d.ts +2 -2
- package/dist/lib/db/local.js +2 -2
- package/dist/lib/db/sql.js +2 -2
- package/dist/lib/db/sqlite-binding.d.ts +22 -0
- package/dist/lib/db/sqlite-binding.js +168 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +12 -0
- package/dist/lib/doctor/checks/anywhere.js +63 -12
- package/dist/lib/doctor/checks/cli-path.d.ts +2 -1
- package/dist/lib/doctor/checks/cli-path.js +2 -1
- package/dist/lib/doctor/checks/hook-engine.js +2 -1
- package/dist/lib/hook-engine/reconcile.d.ts +9 -0
- package/dist/lib/hook-engine/reconcile.js +134 -0
- package/dist/lib/hook-engine/registration.d.ts +17 -0
- package/dist/lib/hook-engine/registration.js +50 -0
- package/dist/lib/install/claude-targets.d.ts +2 -1
- package/dist/lib/install/claude-targets.js +8 -3
- package/dist/lib/install/codex-targets.d.ts +3 -0
- package/dist/lib/install/codex-targets.js +15 -4
- package/dist/lib/install/hook-target.d.ts +9 -0
- package/dist/lib/install/hook-target.js +39 -0
- package/dist/lib/windows/console-encoding.d.ts +24 -0
- package/dist/lib/windows/console-encoding.js +83 -0
- package/dist/lib/windows/install-followup.d.ts +35 -0
- package/dist/lib/windows/install-followup.js +142 -0
- package/dist/lib/windows/user-path.d.ts +80 -3
- package/dist/lib/windows/user-path.js +187 -26
- package/dist/main.js +2 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +88 -0
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +14 -2
- package/dist/utils/hook-trust.d.ts +9 -0
- package/dist/utils/hook-trust.js +30 -2
- package/dist/utils/qmd-native-binding-error.js +3 -1
- package/dist/utils/resolve-executable.d.ts +2 -0
- package/dist/utils/resolve-executable.js +6 -2
- package/package.json +8 -8
|
@@ -3,14 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* npm writes `hq.cmd` directly under its prefix (never `<prefix>\bin`). When
|
|
5
5
|
* that folder is not on the user or machine Path, a new terminal cannot find
|
|
6
|
-
* `hq` even though the install succeeded.
|
|
7
|
-
*
|
|
6
|
+
* `hq` even though the install succeeded. The supported install adds that
|
|
7
|
+
* folder to the user PATH (HKCU only) and broadcasts the change. It never
|
|
8
|
+
* edits the machine PATH. When the add fails, doctor prints the exact command.
|
|
9
|
+
*
|
|
10
|
+
* Windows PowerShell 5.1 and reg.exe print piped output in the console code
|
|
11
|
+
* page, so a Path holding `C:\Users\José` comes back garbled when read as
|
|
12
|
+
* UTF-8. Reads therefore return the value as base64 of its UTF-16LE bytes, and
|
|
13
|
+
* the append is one PowerShell script that reads, checks, and writes the value
|
|
14
|
+
* without it ever leaving PowerShell.
|
|
8
15
|
*/
|
|
9
16
|
import { spawnSync } from "node:child_process";
|
|
10
17
|
import * as fs from "node:fs";
|
|
11
18
|
import * as path from "node:path";
|
|
12
|
-
|
|
13
|
-
const
|
|
19
|
+
/** The longest user Path HQ will write. Longer values break `setx` and some installers. */
|
|
20
|
+
export const MAX_USER_PATH_LENGTH = 2047;
|
|
21
|
+
const REPLACEMENT_CHAR = "\uFFFD";
|
|
22
|
+
/**
|
|
23
|
+
* Tests on Windows point the scripts at a scratch key with this variable. The
|
|
24
|
+
* scripts accept only keys under HKCU:\Software\HQTest\ and skip the
|
|
25
|
+
* broadcast for them.
|
|
26
|
+
*/
|
|
27
|
+
export const USER_PATH_TEST_KEY_ENV = "HQ_USER_PATH_TEST_KEY";
|
|
14
28
|
/** The npm prefix folder above the package's `node_modules`, when `hq.cmd` is there. */
|
|
15
29
|
export function npmShimDir(entry, exists) {
|
|
16
30
|
if (!entry)
|
|
@@ -30,23 +44,37 @@ export function expandWindowsEnv(value, env) {
|
|
|
30
44
|
return found === undefined ? whole : found;
|
|
31
45
|
});
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const SELECT_USER_KEY = [
|
|
48
|
+
"$key = 'HKCU:\\Environment'",
|
|
49
|
+
`$test = $env:${USER_PATH_TEST_KEY_ENV}`,
|
|
50
|
+
"if ($test) { if (-not $test.StartsWith('HKCU:\\Software\\HQTest\\', [StringComparison]::OrdinalIgnoreCase)) { exit 2 }; $key = $test }",
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Prints `U:<base64>` for the unexpanded user Path and `M:<base64>` for the
|
|
54
|
+
* machine Path, base64 of the UTF-16LE bytes, or `U-` / `M-` when a value
|
|
55
|
+
* cannot be read. Base64 is ASCII, so the console code page cannot change it.
|
|
56
|
+
*/
|
|
57
|
+
export const READ_PATHS_SCRIPT = [
|
|
58
|
+
"$ErrorActionPreference = 'Stop'",
|
|
59
|
+
...SELECT_USER_KEY,
|
|
60
|
+
"function Emit($tag, $k) { $i = Get-Item -LiteralPath $k -ErrorAction SilentlyContinue; if ($null -eq $i) { return $tag + '-' }; $v = $i.GetValue('Path', $null, 'DoNotExpandEnvironmentNames'); if ($v -isnot [string]) { return $tag + '-' }; return $tag + ':' + [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($v)) }",
|
|
61
|
+
"Write-Output (Emit 'U' $key)",
|
|
62
|
+
"Write-Output (Emit 'M' 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment')",
|
|
63
|
+
].join("\n");
|
|
64
|
+
/** Decode READ_PATHS_SCRIPT output. Null for a value that could not be read. */
|
|
65
|
+
export function parseReadPathsOutput(stdout) {
|
|
66
|
+
const out = { user: null, machine: null };
|
|
67
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
68
|
+
const match = /^([UM]):([A-Za-z0-9+/=]*)$/.exec(line.trim());
|
|
69
|
+
if (!match)
|
|
70
|
+
continue;
|
|
71
|
+
const value = Buffer.from(match[2], "base64").toString("utf16le");
|
|
72
|
+
if (match[1] === "U")
|
|
73
|
+
out.user = value;
|
|
74
|
+
else
|
|
75
|
+
out.machine = value;
|
|
48
76
|
}
|
|
49
|
-
return
|
|
77
|
+
return out;
|
|
50
78
|
}
|
|
51
79
|
/**
|
|
52
80
|
* The PATH a new terminal will get: the user Path then the machine Path, as
|
|
@@ -54,7 +82,17 @@ function regPath(run, key) {
|
|
|
54
82
|
* be read.
|
|
55
83
|
*/
|
|
56
84
|
export function readPersistedPath(run, env) {
|
|
57
|
-
|
|
85
|
+
let values = [];
|
|
86
|
+
try {
|
|
87
|
+
const result = run(READ_PATHS_SCRIPT, {});
|
|
88
|
+
if (result.status === 0) {
|
|
89
|
+
const read = parseReadPathsOutput(result.stdout);
|
|
90
|
+
values = [read.user, read.machine].filter((v) => v !== null);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// PowerShell missing or refused: treat as unreadable.
|
|
95
|
+
}
|
|
58
96
|
if (values.length === 0)
|
|
59
97
|
return { value: env.PATH ?? env.Path ?? "", source: "process" };
|
|
60
98
|
return { value: values.map((v) => expandWindowsEnv(v, env)).join(";"), source: "registry" };
|
|
@@ -69,8 +107,15 @@ export function isDirOnPath(dir, pathValue) {
|
|
|
69
107
|
const want = normalizeDir(dir);
|
|
70
108
|
return pathValue.split(";").some((entry) => normalizeDir(entry) === want);
|
|
71
109
|
}
|
|
72
|
-
|
|
73
|
-
|
|
110
|
+
/** Runs a script through Windows PowerShell. The script travels as -EncodedCommand (UTF-16LE), so quoting cannot change it. */
|
|
111
|
+
function defaultPowerShell(script, extraEnv) {
|
|
112
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
113
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded], {
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
windowsHide: true,
|
|
116
|
+
timeout: 15_000,
|
|
117
|
+
env: { ...process.env, ...extraEnv },
|
|
118
|
+
});
|
|
74
119
|
return { status: result.status, stdout: result.stdout ?? "" };
|
|
75
120
|
}
|
|
76
121
|
/** Null when not on Windows or when hq was not installed as an npm global shim. */
|
|
@@ -80,7 +125,7 @@ export function checkShimOnPath(deps = {}) {
|
|
|
80
125
|
const shimDir = npmShimDir(deps.entry ?? process.argv[1], deps.exists ?? fs.existsSync);
|
|
81
126
|
if (!shimDir)
|
|
82
127
|
return null;
|
|
83
|
-
const persisted = readPersistedPath(deps.
|
|
128
|
+
const persisted = readPersistedPath(deps.powerShell ?? defaultPowerShell, deps.env ?? process.env);
|
|
84
129
|
return { shimDir, onPath: isDirOnPath(shimDir, persisted.value), source: persisted.source };
|
|
85
130
|
}
|
|
86
131
|
/** Dummy user variable set and cleared so Windows tells running programs that the environment changed. */
|
|
@@ -106,9 +151,125 @@ export function shimPathWarning(check) {
|
|
|
106
151
|
return [];
|
|
107
152
|
return [
|
|
108
153
|
`The folder that holds the hq command (${check.shimDir}) is not on your Windows PATH, so a new terminal will not find hq.`,
|
|
109
|
-
"To add it for your user, run this in PowerShell, then open a new terminal:",
|
|
154
|
+
"HQ could not add it automatically. To add it for your user, run this in PowerShell, then open a new terminal:",
|
|
110
155
|
` ${pathFixCommand(check.shimDir)}`,
|
|
111
|
-
"HQ does not change PATH for you.",
|
|
112
156
|
];
|
|
113
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Decide the new unexpanded user Path. Membership is decided on the expanded
|
|
160
|
+
* value so `%APPDATA%\npm` counts. The stored text keeps `%VARS%`. A value
|
|
161
|
+
* that already holds U+FFFD was garbled by an earlier decode and is never
|
|
162
|
+
* written back. The ensure script applies the same rules inside PowerShell.
|
|
163
|
+
*/
|
|
164
|
+
export function planUserPathAppend(current, dir, env) {
|
|
165
|
+
if (isDirOnPath(dir, expandWindowsEnv(current, env)))
|
|
166
|
+
return { action: "skip", reason: "present" };
|
|
167
|
+
if (current.includes(REPLACEMENT_CHAR) || dir.includes(REPLACEMENT_CHAR) || dir.includes(";")) {
|
|
168
|
+
return { action: "skip", reason: "unsafe-value" };
|
|
169
|
+
}
|
|
170
|
+
const trimmed = current.trim().replace(/;+$/g, "");
|
|
171
|
+
const value = trimmed ? `${trimmed};${dir}` : dir;
|
|
172
|
+
if (value.length > MAX_USER_PATH_LENGTH)
|
|
173
|
+
return { action: "skip", reason: "too-long" };
|
|
174
|
+
return { action: "write", value };
|
|
175
|
+
}
|
|
176
|
+
/** Exit codes of ENSURE_USER_PATH_SCRIPT. */
|
|
177
|
+
export const ENSURE_EXIT = {
|
|
178
|
+
written: 0,
|
|
179
|
+
badInput: 2,
|
|
180
|
+
unreadable: 3,
|
|
181
|
+
unsafeValue: 4,
|
|
182
|
+
tooLong: 5,
|
|
183
|
+
writtenNoBroadcast: 6,
|
|
184
|
+
present: 10,
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Appends $env:HQ_SHIM_DIR to the HKCU Path as REG_EXPAND_SZ, then broadcasts
|
|
188
|
+
* the change. The value is read, checked, and written inside this script, so
|
|
189
|
+
* no console encoding touches it. Mirrors planUserPathAppend.
|
|
190
|
+
*/
|
|
191
|
+
export const ENSURE_USER_PATH_SCRIPT = [
|
|
192
|
+
"$ErrorActionPreference = 'Stop'",
|
|
193
|
+
"$dir = $env:HQ_SHIM_DIR",
|
|
194
|
+
`if ([string]::IsNullOrWhiteSpace($dir) -or $dir.Contains(';') -or $dir.Contains([string][char]0xFFFD)) { exit ${ENSURE_EXIT.badInput} }`,
|
|
195
|
+
...SELECT_USER_KEY,
|
|
196
|
+
"$item = Get-Item -LiteralPath $key -ErrorAction SilentlyContinue",
|
|
197
|
+
`if ($null -eq $item) { exit ${ENSURE_EXIT.unreadable} }`,
|
|
198
|
+
"$p = $item.GetValue('Path', '', 'DoNotExpandEnvironmentNames')",
|
|
199
|
+
`if ($p -isnot [string]) { exit ${ENSURE_EXIT.unsafeValue} }`,
|
|
200
|
+
// Same comparison as normalizeDir: unquote, use backslashes, collapse repeated
|
|
201
|
+
// separators (keeping a UNC prefix), drop trailing ones, ignore case.
|
|
202
|
+
"function Norm($s) { $t = $s.Trim(); if ($t.Length -ge 2 -and $t.StartsWith([string][char]34) -and $t.EndsWith([string][char]34)) { $t = $t.Substring(1, $t.Length - 2).Trim() }; $t = $t.Replace([char]47, [char]92); $bs = [string][char]92; $lead = ''; if ($t.StartsWith($bs + $bs)) { $lead = $bs + $bs; $t = $t.Substring(2) }; $t = $lead + [regex]::Replace($t, ($bs + $bs + '+'), $bs); return $t.TrimEnd([char]92).ToLowerInvariant() }",
|
|
203
|
+
"$want = Norm $dir",
|
|
204
|
+
`foreach ($e in [Environment]::ExpandEnvironmentVariables($p).Split(';')) { if ((Norm $e) -eq $want) { exit ${ENSURE_EXIT.present} } }`,
|
|
205
|
+
`if ($p.Contains([string][char]0xFFFD)) { exit ${ENSURE_EXIT.unsafeValue} }`,
|
|
206
|
+
"$t = $p.Trim().TrimEnd(';')",
|
|
207
|
+
"if ($t) { $new = $t + ';' + $dir } else { $new = $dir }",
|
|
208
|
+
`if ($new.Length -gt ${MAX_USER_PATH_LENGTH}) { exit ${ENSURE_EXIT.tooLong} }`,
|
|
209
|
+
"New-ItemProperty -LiteralPath $key -Name Path -PropertyType ExpandString -Force -Value $new | Out-Null",
|
|
210
|
+
`if (-not $test) { try { [Environment]::SetEnvironmentVariable('${PATH_REFRESH_VAR}','1','User'); [Environment]::SetEnvironmentVariable('${PATH_REFRESH_VAR}',$null,'User') } catch { exit ${ENSURE_EXIT.writtenNoBroadcast} } }`,
|
|
211
|
+
`exit ${ENSURE_EXIT.written}`,
|
|
212
|
+
].join("\n");
|
|
213
|
+
export function ensureExitReason(status) {
|
|
214
|
+
switch (status) {
|
|
215
|
+
case ENSURE_EXIT.written:
|
|
216
|
+
case ENSURE_EXIT.writtenNoBroadcast:
|
|
217
|
+
return { written: true, reason: "written" };
|
|
218
|
+
case ENSURE_EXIT.present:
|
|
219
|
+
return { written: false, reason: "present" };
|
|
220
|
+
case ENSURE_EXIT.unreadable:
|
|
221
|
+
return { written: false, reason: "unreadable" };
|
|
222
|
+
case ENSURE_EXIT.unsafeValue:
|
|
223
|
+
case ENSURE_EXIT.badInput:
|
|
224
|
+
return { written: false, reason: "unsafe-value" };
|
|
225
|
+
case ENSURE_EXIT.tooLong:
|
|
226
|
+
return { written: false, reason: "too-long" };
|
|
227
|
+
default:
|
|
228
|
+
return { written: false, reason: "write-failed" };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export function powershellUserPathStore(run = defaultPowerShell, extraEnv = {}) {
|
|
232
|
+
return {
|
|
233
|
+
appendUserPath(dir) {
|
|
234
|
+
try {
|
|
235
|
+
return ensureExitReason(run(ENSURE_USER_PATH_SCRIPT, { ...extraEnv, HQ_SHIM_DIR: dir }).status);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return { written: false, reason: "write-failed" };
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
let cachedOnPathDir = null;
|
|
244
|
+
export function resetUserPathEnsureCacheForTests() {
|
|
245
|
+
cachedOnPathDir = null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Add the npm shim folder to HKCU Path when it is missing. Null off Windows
|
|
249
|
+
* or when this process is not an npm global install. A failed read does not
|
|
250
|
+
* write. Machine Path is never written.
|
|
251
|
+
*/
|
|
252
|
+
export function ensureShimOnUserPath(deps = {}) {
|
|
253
|
+
const platform = deps.platform ?? process.platform;
|
|
254
|
+
if (platform !== "win32")
|
|
255
|
+
return null;
|
|
256
|
+
const cacheable = deps.store === undefined && deps.powerShell === undefined && deps.platform === undefined;
|
|
257
|
+
if (cacheable && cachedOnPathDir) {
|
|
258
|
+
return { shimDir: cachedOnPathDir, written: false, onPath: true, source: "registry", reason: "present" };
|
|
259
|
+
}
|
|
260
|
+
const check = checkShimOnPath(deps);
|
|
261
|
+
if (!check)
|
|
262
|
+
return null;
|
|
263
|
+
if (check.onPath) {
|
|
264
|
+
if (cacheable)
|
|
265
|
+
cachedOnPathDir = check.shimDir;
|
|
266
|
+
return { shimDir: check.shimDir, written: false, onPath: true, source: check.source, reason: "present" };
|
|
267
|
+
}
|
|
268
|
+
const store = deps.store ?? powershellUserPathStore(deps.powerShell);
|
|
269
|
+
const applied = store.appendUserPath(check.shimDir);
|
|
270
|
+
const onPath = applied.written || applied.reason === "present";
|
|
271
|
+
if (onPath && cacheable)
|
|
272
|
+
cachedOnPathDir = check.shimDir;
|
|
273
|
+
return { shimDir: check.shimDir, written: applied.written, onPath, source: check.source, reason: applied.reason };
|
|
274
|
+
}
|
|
114
275
|
//# sourceMappingURL=user-path.js.map
|
package/dist/main.js
CHANGED
|
@@ -48,6 +48,7 @@ import { enforceVersionGate, getVersionGatePhase, isLocalDependencyInstall, reso
|
|
|
48
48
|
import { autoUpdateAndReexec } from "./utils/self-update.js";
|
|
49
49
|
import { CLI_VERSION } from "./cli-version.js";
|
|
50
50
|
import { findLazyCommand, registerCommandCatalog } from "./lazy-commands.js";
|
|
51
|
+
import { argvWithoutHelpOnUnknownCommand } from "./unknown-command.js";
|
|
51
52
|
import { emitCliFirstSyncNextStepOutcome, emitCliFeatureUsed, emitCliLimitNoticeEngagement, emitCliLimitNoticeExposure, emitCliInviteNudgeEvent, emitCliSessionStarted, emitPlanLimitInviteTeammateExposure, isCliLimitResourceKind, queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, takeFirstSyncStepOutcomes, takeCliFeatureUsages, takeCliLimitNoticeEngagements, takeCliLimitNoticeExposures, takeCliInviteNudgeEvents, takePlanLimitInviteTeammateExposures, maybeShowFirstSyncNextStep, } from "./utils/cli-telemetry.js";
|
|
52
53
|
import { reportCliClientHealthInvocation } from "./utils/client-health.js";
|
|
53
54
|
import { settleWithin } from "./utils/settle-with-timeout.js";
|
|
@@ -308,7 +309,7 @@ export async function runCli() {
|
|
|
308
309
|
return;
|
|
309
310
|
}
|
|
310
311
|
await releasePackageUseLeaseAfterCommandLoad(process.argv);
|
|
311
|
-
await program.parseAsync();
|
|
312
|
+
await program.parseAsync(argvWithoutHelpOnUnknownCommand(process.argv));
|
|
312
313
|
commandParsed = true;
|
|
313
314
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
314
315
|
maybeShowFirstSyncNextStep();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq invite --help` is not a root command. Commander treats `--help` as a
|
|
3
|
+
* request for the parent help and exits 0 before unknownCommand(). Strip help
|
|
4
|
+
* flags when the first command word is not a root, then append a nested
|
|
5
|
+
* did-you-mean when Commander has not already suggested one.
|
|
6
|
+
* A hidden root such as `daemon` is still a real command, so its `--help` stays.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
export declare function firstCommandToken(argv: readonly string[]): string | undefined;
|
|
10
|
+
export declare function isKnownRootCommand(token: string): boolean;
|
|
11
|
+
/** Drop `--help`/`-h` so an unknown first word is reported instead of printing root help. */
|
|
12
|
+
export declare function argvWithoutHelpOnUnknownCommand(argv: readonly string[]): string[];
|
|
13
|
+
/** Nested `parent child` paths whose command name or alias is `token`. Hidden commands are skipped. */
|
|
14
|
+
export declare function suggestNestedCommands(token: string): string[];
|
|
15
|
+
export declare function augmentUnknownCommandError(text: string): string;
|
|
16
|
+
/** Wrap the current stderr writer once. A bare unknown-command line gains a nested suggestion. */
|
|
17
|
+
export declare function installUnknownCommandSuggestions(program: Command): void;
|
|
18
|
+
//# sourceMappingURL=unknown-command.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { COMMAND_CATALOG } from "./command-catalog.generated.js";
|
|
2
|
+
const SUGGEST = Symbol.for("hq.unknownCommandSuggest");
|
|
3
|
+
const ANSI = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
4
|
+
export function firstCommandToken(argv) {
|
|
5
|
+
const args = argv.slice(2);
|
|
6
|
+
for (const token of args) {
|
|
7
|
+
if (token === "--")
|
|
8
|
+
break;
|
|
9
|
+
if (token.startsWith("-"))
|
|
10
|
+
continue;
|
|
11
|
+
return token;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
export function isKnownRootCommand(token) {
|
|
16
|
+
const catalog = COMMAND_CATALOG;
|
|
17
|
+
// Hidden roots are omitted from help, but they are still commands. Stripping
|
|
18
|
+
// `--help` from `hq daemon remote-control-pools --help` makes Commander print
|
|
19
|
+
// that command's usage to stderr and exit 1.
|
|
20
|
+
return catalog.some((command) => command.name === token || command.aliases.includes(token));
|
|
21
|
+
}
|
|
22
|
+
/** Drop `--help`/`-h` so an unknown first word is reported instead of printing root help. */
|
|
23
|
+
export function argvWithoutHelpOnUnknownCommand(argv) {
|
|
24
|
+
const token = firstCommandToken(argv);
|
|
25
|
+
if (!token || token === "help" || isKnownRootCommand(token))
|
|
26
|
+
return [...argv];
|
|
27
|
+
const out = [];
|
|
28
|
+
let passthrough = false;
|
|
29
|
+
argv.forEach((arg, index) => {
|
|
30
|
+
if (index < 2) {
|
|
31
|
+
out.push(arg);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (arg === "--")
|
|
35
|
+
passthrough = true;
|
|
36
|
+
if (!passthrough && (arg === "--help" || arg === "-h"))
|
|
37
|
+
return;
|
|
38
|
+
out.push(arg);
|
|
39
|
+
});
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Nested `parent child` paths whose command name or alias is `token`. Hidden commands are skipped. */
|
|
43
|
+
export function suggestNestedCommands(token) {
|
|
44
|
+
const hits = new Set();
|
|
45
|
+
const walk = (entries, prefix) => {
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.hidden)
|
|
48
|
+
continue;
|
|
49
|
+
const next = [...prefix, entry.name];
|
|
50
|
+
if (prefix.length > 0 && (entry.name === token || entry.aliases.includes(token)))
|
|
51
|
+
hits.add(next.join(" "));
|
|
52
|
+
if (entry.subcommands.length > 0)
|
|
53
|
+
walk(entry.subcommands, next);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
walk(COMMAND_CATALOG, []);
|
|
57
|
+
return [...hits].sort((a, b) => a.localeCompare(b));
|
|
58
|
+
}
|
|
59
|
+
export function augmentUnknownCommandError(text) {
|
|
60
|
+
const plain = text.replace(ANSI, "");
|
|
61
|
+
const match = /^error: unknown command '([^']+)'\n$/.exec(plain);
|
|
62
|
+
if (!match)
|
|
63
|
+
return text;
|
|
64
|
+
const suggestions = suggestNestedCommands(match[1]);
|
|
65
|
+
if (suggestions.length === 0)
|
|
66
|
+
return text;
|
|
67
|
+
const hint = suggestions.length === 1
|
|
68
|
+
? `(Did you mean ${suggestions[0]}?)`
|
|
69
|
+
: `(Did you mean one of ${suggestions.join(", ")}?)`;
|
|
70
|
+
return `${text}${hint}\n`;
|
|
71
|
+
}
|
|
72
|
+
/** Wrap the current stderr writer once. A bare unknown-command line gains a nested suggestion. */
|
|
73
|
+
export function installUnknownCommandSuggestions(program) {
|
|
74
|
+
const tagged = program;
|
|
75
|
+
if (tagged[SUGGEST])
|
|
76
|
+
return;
|
|
77
|
+
tagged[SUGGEST] = true;
|
|
78
|
+
const config = program._outputConfiguration;
|
|
79
|
+
const base = config?.writeErr ?? ((str) => {
|
|
80
|
+
process.stderr.write(str);
|
|
81
|
+
});
|
|
82
|
+
program.configureOutput({
|
|
83
|
+
writeErr(str) {
|
|
84
|
+
base(augmentUnknownCommandError(str));
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=unknown-command.js.map
|
|
@@ -119,6 +119,14 @@ export type EnsureCognitoTokenOptions = {
|
|
|
119
119
|
/** Cancels bounded, non-interactive person-session refreshes. */
|
|
120
120
|
signal?: AbortSignal;
|
|
121
121
|
};
|
|
122
|
+
/**
|
|
123
|
+
* Env switch a parent process sets so a child `hq` never opens a browser
|
|
124
|
+
* sign-in. A hook-driven helper (SessionStart's refresh-vault-access) cannot
|
|
125
|
+
* complete a browser login, and a child waiting on one outlives its caller.
|
|
126
|
+
*/
|
|
127
|
+
export declare const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
|
|
128
|
+
/** An explicit `interactive` option wins; otherwise interactive unless the env switch is "1". */
|
|
129
|
+
export declare function resolveInteractiveLogin(options: Pick<EnsureCognitoTokenOptions, "interactive">, env?: NodeJS.ProcessEnv): boolean;
|
|
122
130
|
/**
|
|
123
131
|
* True when a usable person Cognito cache file exists under the effective
|
|
124
132
|
* `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
|
|
@@ -267,6 +267,18 @@ export function isMachineCredsFileEnvSet(env = process.env) {
|
|
|
267
267
|
export function defaultMachineCredsFile(home = os.homedir()) {
|
|
268
268
|
return path.join(home, ".hq-agent", "machine-creds.json");
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Env switch a parent process sets so a child `hq` never opens a browser
|
|
272
|
+
* sign-in. A hook-driven helper (SessionStart's refresh-vault-access) cannot
|
|
273
|
+
* complete a browser login, and a child waiting on one outlives its caller.
|
|
274
|
+
*/
|
|
275
|
+
export const HQ_NO_BROWSER_LOGIN_ENV = "HQ_NO_BROWSER_LOGIN";
|
|
276
|
+
/** An explicit `interactive` option wins; otherwise interactive unless the env switch is "1". */
|
|
277
|
+
export function resolveInteractiveLogin(options, env = process.env) {
|
|
278
|
+
if (options.interactive !== undefined)
|
|
279
|
+
return options.interactive;
|
|
280
|
+
return env[HQ_NO_BROWSER_LOGIN_ENV]?.trim() !== "1";
|
|
281
|
+
}
|
|
270
282
|
/**
|
|
271
283
|
* Align `process.env.HQ_MACHINE_CREDS_FILE` with a (possibly synthetic) `env`
|
|
272
284
|
* for the duration of `fn`, so hq-cloud's `isMachineIdentity()` sees the same
|
|
@@ -971,7 +983,7 @@ export async function ensureCognitoToken(options = {}) {
|
|
|
971
983
|
return withTokenStateDirSection(() => ensureCognitoTokenUnserialized(options));
|
|
972
984
|
}
|
|
973
985
|
async function ensureCognitoTokenUnserialized(options) {
|
|
974
|
-
const interactive = options
|
|
986
|
+
const interactive = resolveInteractiveLogin(options);
|
|
975
987
|
// Machine identities (company agents) mint sessions on demand via
|
|
976
988
|
// USER_PASSWORD_AUTH — no refresh-token dance, no browser. The vault API's
|
|
977
989
|
// JWT authorizer accepts ID tokens, and the agent's identity claims
|
|
@@ -1062,7 +1074,7 @@ export async function ensureCognitoIdToken(options = {}) {
|
|
|
1062
1074
|
return withTokenStateDirSection(() => ensureCognitoIdTokenUnserialized(options));
|
|
1063
1075
|
}
|
|
1064
1076
|
async function ensureCognitoIdTokenUnserialized(options) {
|
|
1065
|
-
const interactive = options
|
|
1077
|
+
const interactive = resolveInteractiveLogin(options);
|
|
1066
1078
|
if (options.signal?.aborted)
|
|
1067
1079
|
throw options.signal.reason ?? new Error("Token acquisition was aborted");
|
|
1068
1080
|
if (wantsMachineCognitoTokens(options)) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findExecutableOnPath } from './resolve-executable.js';
|
|
2
|
+
import { type SpawnPlan } from './windows-spawn.js';
|
|
1
3
|
export interface CodexRpcClient {
|
|
2
4
|
request(method: string, params: unknown): Promise<unknown>;
|
|
3
5
|
close(): Promise<void>;
|
|
@@ -49,6 +51,13 @@ export interface RuntimeHookTrustResult {
|
|
|
49
51
|
trusted: number;
|
|
50
52
|
reason?: string;
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* How to launch Codex. npm installs Codex on Windows as a `codex.cmd` shim,
|
|
56
|
+
* which `spawn('codex')` cannot find (ENOENT) and Node refuses to run without a
|
|
57
|
+
* shell, so a bare name is resolved through PATH + PATHEXT and a batch shim is
|
|
58
|
+
* routed through the escaped cmd.exe plan.
|
|
59
|
+
*/
|
|
60
|
+
export declare function planCodexLaunch(executable: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, find?: typeof findExecutableOnPath): SpawnPlan;
|
|
52
61
|
/** Start a short-lived Codex app-server and complete its JSONL handshake. */
|
|
53
62
|
export declare function createCodexAppServerClient(cwd: string, executable?: string, args?: string[]): Promise<CodexRpcClient>;
|
|
54
63
|
/** Trust only hooks declared by this HQ root's project `.codex/` layer. */
|
package/dist/utils/hook-trust.js
CHANGED
|
@@ -8,6 +8,8 @@ import * as readline from 'node:readline';
|
|
|
8
8
|
// read->merge->write, backup before the first byte, temp+fsync+rename commit)
|
|
9
9
|
// rather than a bare readFile/writeFile pair.
|
|
10
10
|
import { acquireLock, atomicReplace, backupConfig, realpathOrSelf, releaseLock, restoreFromBackup, } from '../commands/mcp-registration.js';
|
|
11
|
+
import { findExecutableOnPath } from './resolve-executable.js';
|
|
12
|
+
import { planResolvedCommandSpawn } from './windows-spawn.js';
|
|
11
13
|
/**
|
|
12
14
|
* The home directory every runtime's config is resolved under.
|
|
13
15
|
*
|
|
@@ -23,6 +25,11 @@ import { acquireLock, atomicReplace, backupConfig, realpathOrSelf, releaseLock,
|
|
|
23
25
|
export function defaultHomeDir() {
|
|
24
26
|
return process.env.HOME?.trim() ? process.env.HOME : os.homedir();
|
|
25
27
|
}
|
|
28
|
+
const RUNTIME_LABEL = {
|
|
29
|
+
codex: 'Codex',
|
|
30
|
+
grok: 'Grok',
|
|
31
|
+
claude: 'Claude Code',
|
|
32
|
+
};
|
|
26
33
|
const CODEX_REQUEST_TIMEOUT_MS = 10_000;
|
|
27
34
|
function errorMessage(value) {
|
|
28
35
|
return value instanceof Error ? value.message : String(value);
|
|
@@ -77,11 +84,32 @@ function hqProjectHooks(hqRoot, hooks) {
|
|
|
77
84
|
!hook.isManaged &&
|
|
78
85
|
isWithin(path.resolve(hook.sourcePath), codexLayer));
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* How to launch Codex. npm installs Codex on Windows as a `codex.cmd` shim,
|
|
89
|
+
* which `spawn('codex')` cannot find (ENOENT) and Node refuses to run without a
|
|
90
|
+
* shell, so a bare name is resolved through PATH + PATHEXT and a batch shim is
|
|
91
|
+
* routed through the escaped cmd.exe plan.
|
|
92
|
+
*/
|
|
93
|
+
export function planCodexLaunch(executable, args, env = process.env, platform = process.platform, find = findExecutableOnPath) {
|
|
94
|
+
let command = executable;
|
|
95
|
+
if (platform === 'win32' && !path.win32.isAbsolute(executable) && !/[\\/]/.test(executable)) {
|
|
96
|
+
const resolved = find(executable, env.PATH ?? env.Path ?? '', platform, env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM');
|
|
97
|
+
if (!resolved) {
|
|
98
|
+
throw new Error(`Codex CLI not found on PATH (looked for ${executable}.exe and ${executable}.cmd); install Codex or add it to PATH, then rerun \`hq reindex\``);
|
|
99
|
+
}
|
|
100
|
+
command = resolved;
|
|
101
|
+
}
|
|
102
|
+
return planResolvedCommandSpawn(command, args, env, platform);
|
|
103
|
+
}
|
|
80
104
|
/** Start a short-lived Codex app-server and complete its JSONL handshake. */
|
|
81
105
|
export async function createCodexAppServerClient(cwd, executable = 'codex', args = ['app-server']) {
|
|
82
|
-
const
|
|
106
|
+
const plan = planCodexLaunch(executable, args);
|
|
107
|
+
const child = spawn(plan.cmd, plan.args, {
|
|
83
108
|
cwd,
|
|
84
109
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
110
|
+
shell: plan.shell,
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
85
113
|
});
|
|
86
114
|
const lines = readline.createInterface({ input: child.stdout });
|
|
87
115
|
const pending = new Map();
|
|
@@ -692,7 +720,7 @@ export async function trustHqRuntimeHooks(hqRoot, deps = DEFAULT_DEPS, options =
|
|
|
692
720
|
}
|
|
693
721
|
}
|
|
694
722
|
else if (result.status === 'failed') {
|
|
695
|
-
console.warn(`reindex: could not trust ${result.runtime} HQ hooks: ${result.reason ?? 'unknown error'}
|
|
723
|
+
console.warn(`reindex: could not trust ${result.runtime} HQ hooks: ${result.reason ?? 'unknown error'}. ${RUNTIME_LABEL[result.runtime]} will not run HQ hooks until trust succeeds; fix the error above and rerun \`hq reindex\`.`);
|
|
696
724
|
}
|
|
697
725
|
}
|
|
698
726
|
return results;
|
|
@@ -48,7 +48,9 @@ export const QMD_NATIVE_BINDING_REMEDY = "hq's local search index can't start: i
|
|
|
48
48
|
"not built during install. pnpm 10 skips dependency build scripts by default, " +
|
|
49
49
|
"which leaves better-sqlite3 uncompiled. Approve the build and reinstall — for " +
|
|
50
50
|
"a global install run `pnpm approve-builds -g better-sqlite3` (drop -g for a " +
|
|
51
|
-
"local project)
|
|
51
|
+
"local project). npm 11.16 and later skip install scripts unless you opt in; reinstall with " +
|
|
52
|
+
"`npm install -g @indigoai-us/hq-cli --allow-scripts=better-sqlite3`. " +
|
|
53
|
+
"Or reinstall hq with npm, then run the command again.";
|
|
52
54
|
/**
|
|
53
55
|
* The text to inspect. For a QmdExitError the captured stderr/stdout is the qmd
|
|
54
56
|
* process's real diagnostic; the synthesized `message` (which embeds the user's
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** First executable file on PATH for `command` (PATHEXT-expanded on Windows), or undefined. */
|
|
2
|
+
export declare function findExecutableOnPath(command: string, pathValue?: string, platform?: NodeJS.Platform, pathExt?: string): string | undefined;
|
|
1
3
|
export declare function isExecutableOnPath(command: string, pathValue?: string, platform?: NodeJS.Platform, pathExt?: string): boolean;
|
|
2
4
|
//# sourceMappingURL=resolve-executable.d.ts.map
|
|
@@ -9,8 +9,9 @@ function executableCandidates(command, pathValue = process.env.PATH ?? '', platf
|
|
|
9
9
|
: pathValue.split(platform === 'win32' ? ';' : path.delimiter).filter(Boolean).flatMap((directory) => extensions.map((extension) => path.join(directory, `${command}${extension}`)));
|
|
10
10
|
return candidates;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** First executable file on PATH for `command` (PATHEXT-expanded on Windows), or undefined. */
|
|
13
|
+
export function findExecutableOnPath(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
|
|
14
|
+
return executableCandidates(command, pathValue, platform, pathExt).find((candidate) => {
|
|
14
15
|
try {
|
|
15
16
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
16
17
|
return fs.statSync(candidate).isFile();
|
|
@@ -20,4 +21,7 @@ export function isExecutableOnPath(command, pathValue = process.env.PATH ?? '',
|
|
|
20
21
|
}
|
|
21
22
|
});
|
|
22
23
|
}
|
|
24
|
+
export function isExecutableOnPath(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
|
|
25
|
+
return findExecutableOnPath(command, pathValue, platform, pathExt) !== undefined;
|
|
26
|
+
}
|
|
23
27
|
//# sourceMappingURL=resolve-executable.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.149",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"check:lanes-module-cycles": "node scripts/check-lanes-module-cycles.mjs",
|
|
43
43
|
"vitest": "vitest",
|
|
44
44
|
"clean": "rm -rf dist",
|
|
45
|
-
"postinstall": "node scripts/install-hook-engine.mjs
|
|
45
|
+
"postinstall": "node scripts/install-hook-engine.mjs || node -e \"process.exit(0)\"",
|
|
46
46
|
"prepare": "husky || true",
|
|
47
47
|
"check:no-hermes": "bash scripts/check-no-hermes.sh",
|
|
48
48
|
"prepack": "node scripts/hook-engine-manifest.mjs",
|
|
@@ -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.149",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.149",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.149",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.149",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.149",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.149"
|
|
123
123
|
}
|
|
124
124
|
}
|