@indigoai-us/hq-cli 5.345.148 → 5.345.150
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 +75 -0
- package/README.md +2 -0
- package/dist/command-catalog.generated.d.ts +62 -14
- package/dist/command-catalog.generated.js +74 -14
- package/dist/command-help.js +2 -1
- package/dist/commands/_patterns.d.ts +6 -0
- package/dist/commands/_patterns.js +6 -0
- 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/groups.js +9 -6
- package/dist/commands/install-global.d.ts +1 -8
- package/dist/commands/install-global.js +11 -27
- package/dist/commands/lanes-questions.js +7 -4
- package/dist/commands/lanes.js +13 -6
- 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/members.d.ts +35 -0
- package/dist/commands/members.js +65 -14
- package/dist/commands/reindex.js +19 -0
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +11 -6
- package/dist/hook-engine-path.js +10 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +63282 -62480
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- 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/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/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/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/store.d.ts +11 -0
- package/dist/lib/lanes/store.js +20 -0
- package/dist/lib/lanes/watch.js +5 -3
- 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 +9 -9
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work that npm 11 will not run from postinstall: put the hq shim folder on
|
|
3
|
+
* the user PATH, and point the mesh daemon at this install once per CLI version.
|
|
4
|
+
* Failures are ignored. Hook, version, and staged-install routes do not run it.
|
|
5
|
+
*
|
|
6
|
+
* Both steps record their result in ~/.hq/install-followup.json, so later
|
|
7
|
+
* commands start no PowerShell or daemon process for the same version.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { ensureShimOnUserPath, npmShimDir } from "./user-path.js";
|
|
15
|
+
const MARKER_NAME = "install-followup.json";
|
|
16
|
+
export function isInstallFollowupSkipped(argv, env) {
|
|
17
|
+
if (env.HQ_INSTALL_FOLLOWUP === "1" || env.HQ_STAGED_INSTALL === "1")
|
|
18
|
+
return true;
|
|
19
|
+
const args = argv.slice(2).filter((arg) => arg !== "--verbose");
|
|
20
|
+
if (args[0] === "mesh" && args[1] === "daemon")
|
|
21
|
+
return true;
|
|
22
|
+
if (args[0] === "daemon" && (args[1] === "run" || args[1] === "install"))
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
export function isInstalledPackageEntry(entry) {
|
|
27
|
+
return Boolean(entry && /node_modules[/\\]@indigoai-us[/\\]hq-cli[/\\]/i.test(entry));
|
|
28
|
+
}
|
|
29
|
+
function packageVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = fileURLToPath(new URL("../../../package.json", import.meta.url));
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
33
|
+
return typeof pkg.version === "string" ? pkg.version : "";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** A failed daemon install blocks for up to 30 s, so it is retried at most hourly. */
|
|
40
|
+
export const MESH_RETRY_MS = 60 * 60 * 1000;
|
|
41
|
+
function markerFile(env) {
|
|
42
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
43
|
+
return path.join(home, ".hq", MARKER_NAME);
|
|
44
|
+
}
|
|
45
|
+
function readMarker(env) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(fs.readFileSync(markerFile(env), "utf8"));
|
|
48
|
+
const marker = {};
|
|
49
|
+
if (typeof parsed.version === "string")
|
|
50
|
+
marker.version = parsed.version;
|
|
51
|
+
if (typeof parsed.path?.dir === "string" && typeof parsed.path.version === "string") {
|
|
52
|
+
marker.path = { dir: parsed.path.dir, version: parsed.path.version };
|
|
53
|
+
}
|
|
54
|
+
if (typeof parsed.meshFailedVersion === "string" && typeof parsed.meshFailedAt === "number") {
|
|
55
|
+
marker.meshFailedVersion = parsed.meshFailedVersion;
|
|
56
|
+
marker.meshFailedAt = parsed.meshFailedAt;
|
|
57
|
+
}
|
|
58
|
+
return marker;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function writeMarker(env, marker) {
|
|
65
|
+
const file = markerFile(env);
|
|
66
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
67
|
+
fs.writeFileSync(file, `${JSON.stringify(marker)}\n`);
|
|
68
|
+
}
|
|
69
|
+
/** True only when `hq mesh daemon install --if-stale` exited 0. */
|
|
70
|
+
function defaultSpawnMesh(argv, env) {
|
|
71
|
+
const entry = argv[1];
|
|
72
|
+
if (!entry)
|
|
73
|
+
return false;
|
|
74
|
+
const result = spawnSync(process.execPath, [entry, "mesh", "daemon", "install", "--if-stale"], {
|
|
75
|
+
env: { ...env, HQ_INSTALL_FOLLOWUP: "1" },
|
|
76
|
+
timeout: 30_000,
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
stdio: "ignore",
|
|
79
|
+
});
|
|
80
|
+
return result.status === 0 && !result.error;
|
|
81
|
+
}
|
|
82
|
+
export function runWindowsInstallFollowup(deps) {
|
|
83
|
+
const env = deps.env ?? process.env;
|
|
84
|
+
if (isInstallFollowupSkipped(deps.argv, env))
|
|
85
|
+
return;
|
|
86
|
+
const version = deps.version ?? packageVersion();
|
|
87
|
+
if (!version)
|
|
88
|
+
return;
|
|
89
|
+
const read = deps.readMarker ?? (() => readMarker(env));
|
|
90
|
+
const write = deps.writeMarker ?? ((marker) => writeMarker(env, marker));
|
|
91
|
+
const save = (update) => {
|
|
92
|
+
try {
|
|
93
|
+
write({ ...read(), ...update });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// A marker failure only means the next command tries again.
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
let shimDir;
|
|
100
|
+
try {
|
|
101
|
+
shimDir = (deps.shimDir ?? (() => npmShimDir(deps.argv[1], fs.existsSync)))();
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
shimDir = null;
|
|
105
|
+
}
|
|
106
|
+
const marker = read();
|
|
107
|
+
if (shimDir && !(marker.path?.dir === shimDir && marker.path.version === version)) {
|
|
108
|
+
try {
|
|
109
|
+
(deps.ensurePath ?? (() => {
|
|
110
|
+
ensureShimOnUserPath({ entry: deps.argv[1], env });
|
|
111
|
+
}))();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// PATH repair must not block the command the user asked for.
|
|
115
|
+
}
|
|
116
|
+
// Recorded whatever the outcome: a refused or locked HKCU is retried after
|
|
117
|
+
// the next upgrade, and `hq doctor` keeps reporting the missing folder.
|
|
118
|
+
save({ path: { dir: shimDir, version } });
|
|
119
|
+
}
|
|
120
|
+
if (!isInstalledPackageEntry(deps.argv[1]))
|
|
121
|
+
return;
|
|
122
|
+
const mesh = read();
|
|
123
|
+
if (mesh.version === version)
|
|
124
|
+
return;
|
|
125
|
+
const now = (deps.now ?? Date.now)();
|
|
126
|
+
if (mesh.meshFailedVersion === version && mesh.meshFailedAt !== undefined && now - mesh.meshFailedAt < MESH_RETRY_MS)
|
|
127
|
+
return;
|
|
128
|
+
let ok = false;
|
|
129
|
+
try {
|
|
130
|
+
ok = (deps.spawnMesh ?? (() => defaultSpawnMesh(deps.argv, env)))();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// The daemon install is best-effort, same as the old postinstall `|| true`.
|
|
134
|
+
}
|
|
135
|
+
// Only a successful install marks the version done. A failure is retried
|
|
136
|
+
// after MESH_RETRY_MS.
|
|
137
|
+
if (ok)
|
|
138
|
+
save({ version, meshFailedVersion: undefined, meshFailedAt: undefined });
|
|
139
|
+
else
|
|
140
|
+
save({ meshFailedVersion: version, meshFailedAt: now });
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=install-followup.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type PowerShellRun = (script: string, env: NodeJS.ProcessEnv) => {
|
|
2
2
|
status: number | null;
|
|
3
3
|
stdout: string;
|
|
4
4
|
};
|
|
@@ -14,19 +14,38 @@ export interface ShimPathDeps {
|
|
|
14
14
|
/** The running CLI entry script (process.argv[1]). */
|
|
15
15
|
entry?: string;
|
|
16
16
|
exists?: (p: string) => boolean;
|
|
17
|
-
|
|
17
|
+
powerShell?: PowerShellRun;
|
|
18
18
|
env?: NodeJS.ProcessEnv;
|
|
19
19
|
}
|
|
20
|
+
/** The longest user Path HQ will write. Longer values break `setx` and some installers. */
|
|
21
|
+
export declare const MAX_USER_PATH_LENGTH = 2047;
|
|
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 declare const USER_PATH_TEST_KEY_ENV = "HQ_USER_PATH_TEST_KEY";
|
|
20
28
|
/** The npm prefix folder above the package's `node_modules`, when `hq.cmd` is there. */
|
|
21
29
|
export declare function npmShimDir(entry: string | undefined, exists: (p: string) => boolean): string | null;
|
|
22
30
|
/** Expand `%NAME%` references the way Windows does, matching names case-insensitively. */
|
|
23
31
|
export declare function expandWindowsEnv(value: string, env: NodeJS.ProcessEnv): string;
|
|
32
|
+
/**
|
|
33
|
+
* Prints `U:<base64>` for the unexpanded user Path and `M:<base64>` for the
|
|
34
|
+
* machine Path, base64 of the UTF-16LE bytes, or `U-` / `M-` when a value
|
|
35
|
+
* cannot be read. Base64 is ASCII, so the console code page cannot change it.
|
|
36
|
+
*/
|
|
37
|
+
export declare const READ_PATHS_SCRIPT: string;
|
|
38
|
+
/** Decode READ_PATHS_SCRIPT output. Null for a value that could not be read. */
|
|
39
|
+
export declare function parseReadPathsOutput(stdout: string): {
|
|
40
|
+
user: string | null;
|
|
41
|
+
machine: string | null;
|
|
42
|
+
};
|
|
24
43
|
/**
|
|
25
44
|
* The PATH a new terminal will get: the user Path then the machine Path, as
|
|
26
45
|
* stored in the registry. Falls back to this process's PATH when neither can
|
|
27
46
|
* be read.
|
|
28
47
|
*/
|
|
29
|
-
export declare function readPersistedPath(run:
|
|
48
|
+
export declare function readPersistedPath(run: PowerShellRun, env: NodeJS.ProcessEnv): {
|
|
30
49
|
value: string;
|
|
31
50
|
source: "registry" | "process";
|
|
32
51
|
};
|
|
@@ -45,4 +64,62 @@ export declare const PATH_REFRESH_VAR = "HQ_PATH_REFRESH";
|
|
|
45
64
|
export declare function pathFixCommand(dir: string): string;
|
|
46
65
|
/** Lines to print when the shim folder is missing from PATH; empty when it is present. */
|
|
47
66
|
export declare function shimPathWarning(check: ShimPathCheck): string[];
|
|
67
|
+
export type UserPathWriteReason = "unreadable" | "present" | "written" | "write-failed" | "unsafe-value" | "too-long";
|
|
68
|
+
export interface UserPathWriteResult {
|
|
69
|
+
written: boolean;
|
|
70
|
+
reason: UserPathWriteReason;
|
|
71
|
+
}
|
|
72
|
+
/** Adds a folder to HKCU Path in one step: read, check, and write happen together. */
|
|
73
|
+
export interface UserPathStore {
|
|
74
|
+
appendUserPath(dir: string): UserPathWriteResult;
|
|
75
|
+
}
|
|
76
|
+
export type UserPathAppendPlan = {
|
|
77
|
+
action: "write";
|
|
78
|
+
value: string;
|
|
79
|
+
} | {
|
|
80
|
+
action: "skip";
|
|
81
|
+
reason: "present" | "unsafe-value" | "too-long";
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Decide the new unexpanded user Path. Membership is decided on the expanded
|
|
85
|
+
* value so `%APPDATA%\npm` counts. The stored text keeps `%VARS%`. A value
|
|
86
|
+
* that already holds U+FFFD was garbled by an earlier decode and is never
|
|
87
|
+
* written back. The ensure script applies the same rules inside PowerShell.
|
|
88
|
+
*/
|
|
89
|
+
export declare function planUserPathAppend(current: string, dir: string, env: NodeJS.ProcessEnv): UserPathAppendPlan;
|
|
90
|
+
/** Exit codes of ENSURE_USER_PATH_SCRIPT. */
|
|
91
|
+
export declare const ENSURE_EXIT: {
|
|
92
|
+
readonly written: 0;
|
|
93
|
+
readonly badInput: 2;
|
|
94
|
+
readonly unreadable: 3;
|
|
95
|
+
readonly unsafeValue: 4;
|
|
96
|
+
readonly tooLong: 5;
|
|
97
|
+
readonly writtenNoBroadcast: 6;
|
|
98
|
+
readonly present: 10;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Appends $env:HQ_SHIM_DIR to the HKCU Path as REG_EXPAND_SZ, then broadcasts
|
|
102
|
+
* the change. The value is read, checked, and written inside this script, so
|
|
103
|
+
* no console encoding touches it. Mirrors planUserPathAppend.
|
|
104
|
+
*/
|
|
105
|
+
export declare const ENSURE_USER_PATH_SCRIPT: string;
|
|
106
|
+
export declare function ensureExitReason(status: number | null): UserPathWriteResult;
|
|
107
|
+
export declare function powershellUserPathStore(run?: PowerShellRun, extraEnv?: NodeJS.ProcessEnv): UserPathStore;
|
|
108
|
+
export interface EnsureShimResult {
|
|
109
|
+
shimDir: string;
|
|
110
|
+
written: boolean;
|
|
111
|
+
onPath: boolean;
|
|
112
|
+
source: ShimPathCheck["source"];
|
|
113
|
+
reason: UserPathWriteReason;
|
|
114
|
+
}
|
|
115
|
+
export interface EnsureShimDeps extends ShimPathDeps {
|
|
116
|
+
store?: UserPathStore;
|
|
117
|
+
}
|
|
118
|
+
export declare function resetUserPathEnsureCacheForTests(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Add the npm shim folder to HKCU Path when it is missing. Null off Windows
|
|
121
|
+
* or when this process is not an npm global install. A failed read does not
|
|
122
|
+
* write. Machine Path is never written.
|
|
123
|
+
*/
|
|
124
|
+
export declare function ensureShimOnUserPath(deps?: EnsureShimDeps): EnsureShimResult | null;
|
|
48
125
|
//# sourceMappingURL=user-path.d.ts.map
|
|
@@ -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
|