@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
|
@@ -23,6 +23,9 @@ export declare function addAgentsMdBlock(current: string | null, charter: string
|
|
|
23
23
|
/** Strip the HQ block, plus the separator `addAgentsMdBlock` put before it. */
|
|
24
24
|
export declare function removeAgentsMdBlock(current: string): string;
|
|
25
25
|
type Doc = Record<string, unknown>;
|
|
26
|
+
export declare function isAdapterEntry(entry: unknown, hqRoot: string): boolean;
|
|
27
|
+
/** The HQ hooks.json entry for one event and target. */
|
|
28
|
+
export declare function codexHookEntry(hqRoot: string, event: string, target?: HookTarget): Record<string, unknown>;
|
|
26
29
|
/**
|
|
27
30
|
* Add one HQ hook group per event; idempotent. An HQ entry for the other
|
|
28
31
|
* target (adapter vs the hqd shim) is swapped out. Foreign hooks are untouched.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* module never writes a second adapter.
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import { engineArgs, isNativeEntry, quotePosix, quoteWindows } from '../hook-engine/registration.js';
|
|
13
14
|
import * as path from 'path';
|
|
14
15
|
import { shimPath } from './claude-targets.js';
|
|
15
16
|
/** Events the Codex hook adapter is registered for at user scope. */
|
|
@@ -88,15 +89,25 @@ export function removeAgentsMdBlock(current) {
|
|
|
88
89
|
start -= 1;
|
|
89
90
|
return current.slice(0, start) + current.slice(end);
|
|
90
91
|
}
|
|
91
|
-
function isAdapterEntry(entry, hqRoot) {
|
|
92
|
+
export function isAdapterEntry(entry, hqRoot) {
|
|
92
93
|
if (!entry || typeof entry !== 'object')
|
|
93
94
|
return false;
|
|
95
|
+
if (isNativeEntry(entry, hqRoot))
|
|
96
|
+
return true;
|
|
94
97
|
const cmd = entry.command;
|
|
95
98
|
return (typeof cmd === 'string' &&
|
|
96
99
|
(cmd.includes(JSON.stringify(codexAdapterPath(hqRoot))) ||
|
|
97
100
|
cmd.includes(JSON.stringify(shimPath(hqRoot))) ||
|
|
98
101
|
cmd.includes(JSON.stringify(path.join(hqRoot, 'core', 'packages', 'hq-anywhere', 'hooks', 'codex.sh')))));
|
|
99
102
|
}
|
|
103
|
+
/** The HQ hooks.json entry for one event and target. */
|
|
104
|
+
export function codexHookEntry(hqRoot, event, target = 'master') {
|
|
105
|
+
const timeout = event === 'SessionEnd' ? 3 : 300;
|
|
106
|
+
if (typeof target !== 'object')
|
|
107
|
+
return { type: 'command', command: codexHookCommand(hqRoot, target), timeout };
|
|
108
|
+
const argv = [target.engine, ...engineArgs(hqRoot, event, 'codex')];
|
|
109
|
+
return { type: 'command', command: argv.map(quotePosix).join(' '), ...(target.engine.endsWith('.exe') ? { commandWindows: '& ' + argv.map(quoteWindows).join(' ') } : {}), timeout };
|
|
110
|
+
}
|
|
100
111
|
/**
|
|
101
112
|
* Add one HQ hook group per event; idempotent. An HQ entry for the other
|
|
102
113
|
* target (adapter vs the hqd shim) is swapped out. Foreign hooks are untouched.
|
|
@@ -104,14 +115,14 @@ function isAdapterEntry(entry, hqRoot) {
|
|
|
104
115
|
export function addCodexHooks(doc, hqRoot, target = 'master') {
|
|
105
116
|
const hooks = { ...(doc.hooks ?? {}) };
|
|
106
117
|
let changed = false;
|
|
107
|
-
const want = codexHookCommand(hqRoot, target);
|
|
108
118
|
for (const event of CODEX_HOOK_EVENTS) {
|
|
119
|
+
const want = codexHookEntry(hqRoot, event, target);
|
|
109
120
|
let groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
110
|
-
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want));
|
|
121
|
+
const present = groups.some((g) => Array.isArray(g?.hooks) && g.hooks.some((h) => h.command === want.command && h.commandWindows === ('commandWindows' in want ? want.commandWindows : undefined)));
|
|
111
122
|
if (present)
|
|
112
123
|
continue;
|
|
113
124
|
groups = removeCodexHooks({ hooks: { [event]: groups } }, hqRoot).hooks?.[event] ?? [];
|
|
114
|
-
groups.push({ hooks: [
|
|
125
|
+
groups.push({ hooks: [want] });
|
|
115
126
|
hooks[event] = groups;
|
|
116
127
|
changed = true;
|
|
117
128
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type HookTarget } from './claude-targets.js';
|
|
2
|
+
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
3
|
+
export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
6
|
+
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
|
|
9
|
+
//# sourceMappingURL=hook-target.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which hook target a user-scope registration uses. Kept apart from
|
|
3
|
+
* install-global so hq reindex can reconcile registrations without loading the
|
|
4
|
+
* install command graph.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { realpathOrSelf } from '../../commands/mcp-registration.js';
|
|
10
|
+
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../daemon/install.js';
|
|
11
|
+
import { readDaemonConfig } from '../daemon/config.js';
|
|
12
|
+
import { daemonPaths } from '../daemon/paths.js';
|
|
13
|
+
import { registrationEngine } from '../hook-engine/registration.js';
|
|
14
|
+
import { shimPath } from './claude-targets.js';
|
|
15
|
+
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
16
|
+
export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
|
|
17
|
+
if (platform === 'win32')
|
|
18
|
+
return queryTask(DAEMON_WINDOWS_TASK);
|
|
19
|
+
const candidates = platform === 'darwin'
|
|
20
|
+
? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
|
|
21
|
+
: platform === 'linux'
|
|
22
|
+
? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
|
|
23
|
+
: [];
|
|
24
|
+
return candidates.some((p) => fs.existsSync(p));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* US-010: with hqd installed (and the checkout shipping the shim), hooks go to
|
|
28
|
+
* the hqd shim; otherwise to master-hook.sh / the Codex adapter.
|
|
29
|
+
*/
|
|
30
|
+
export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
|
|
31
|
+
const engine = registrationEngine(home, env);
|
|
32
|
+
if (engine)
|
|
33
|
+
return { engine };
|
|
34
|
+
const installed = daemonInstalled ?? isDaemonInstalled(home);
|
|
35
|
+
const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
|
|
36
|
+
const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
|
|
37
|
+
return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=hook-target.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** ASCII stand-ins for characters cmd.exe cannot show on code page 437. */
|
|
2
|
+
export declare function toAscii(text: string): string;
|
|
3
|
+
/** Parse `chcp` output (`Active code page: 437`). Null when the number is missing. */
|
|
4
|
+
export declare function parseChcpOutput(text: string): number | null;
|
|
5
|
+
type ChcpRun = () => {
|
|
6
|
+
status: number | null;
|
|
7
|
+
stdout: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function readConsoleCodePage(run?: ChcpRun): number | null;
|
|
10
|
+
export interface ConsoleEncodingDeps {
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
stdout?: NodeJS.WriteStream;
|
|
13
|
+
stderr?: NodeJS.WriteStream;
|
|
14
|
+
/** Known code page. Null means the page could not be read and is treated as not UTF-8. */
|
|
15
|
+
codePage?: number | null;
|
|
16
|
+
readCodePage?: () => number | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wrap console stdout and stderr on Windows when the code page is not UTF-8.
|
|
20
|
+
* Returns false when no wrap was installed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function installWindowsConsoleEncoding(deps?: ConsoleEncodingDeps): boolean;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=console-encoding.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cmd.exe on a non-UTF-8 code page prints UTF-8 em dashes as mojibake.
|
|
3
|
+
* When stdout or stderr is a console and the code page is not 65001, string
|
|
4
|
+
* writes are transliterated to ASCII. Pipes are left as UTF-8 so captured
|
|
5
|
+
* logs stay intact. Buffer writes are not modified.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
const WRAP = Symbol.for("hq.windowsConsoleEncoding");
|
|
9
|
+
const REPLACEMENTS = [
|
|
10
|
+
[/\u2014/g, "--"],
|
|
11
|
+
[/\u2013/g, "-"],
|
|
12
|
+
[/\u2018|\u2019|\u201A/g, "'"],
|
|
13
|
+
[/\u201C|\u201D|\u201E/g, "\""],
|
|
14
|
+
[/\u2026/g, "..."],
|
|
15
|
+
[/\u00A0/g, " "],
|
|
16
|
+
[/\u2022/g, "*"],
|
|
17
|
+
[/\u2192/g, "->"],
|
|
18
|
+
[/\u2190/g, "<-"],
|
|
19
|
+
[/\u21D2/g, "=>"],
|
|
20
|
+
[/\u00D7/g, "x"],
|
|
21
|
+
];
|
|
22
|
+
/** ASCII stand-ins for characters cmd.exe cannot show on code page 437. */
|
|
23
|
+
export function toAscii(text) {
|
|
24
|
+
let out = text;
|
|
25
|
+
for (const [pattern, replacement] of REPLACEMENTS)
|
|
26
|
+
out = out.replace(pattern, replacement);
|
|
27
|
+
return out.replace(/[^\t\n\r\x20-\x7E]/g, "?");
|
|
28
|
+
}
|
|
29
|
+
/** Parse `chcp` output (`Active code page: 437`). Null when the number is missing. */
|
|
30
|
+
export function parseChcpOutput(text) {
|
|
31
|
+
const match = /:\s*(\d+)/.exec(text);
|
|
32
|
+
if (!match)
|
|
33
|
+
return null;
|
|
34
|
+
const codePage = Number(match[1]);
|
|
35
|
+
return Number.isInteger(codePage) ? codePage : null;
|
|
36
|
+
}
|
|
37
|
+
function defaultChcp() {
|
|
38
|
+
const result = spawnSync("chcp", [], { encoding: "utf8", windowsHide: true, timeout: 2000 });
|
|
39
|
+
return { status: result.status, stdout: result.stdout ?? "" };
|
|
40
|
+
}
|
|
41
|
+
export function readConsoleCodePage(run = defaultChcp) {
|
|
42
|
+
try {
|
|
43
|
+
const result = run();
|
|
44
|
+
if (result.status !== 0)
|
|
45
|
+
return null;
|
|
46
|
+
return parseChcpOutput(result.stdout);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function wrapStream(stream) {
|
|
53
|
+
if (stream[WRAP])
|
|
54
|
+
return;
|
|
55
|
+
const original = stream.write.bind(stream);
|
|
56
|
+
stream.write = ((chunk, encoding, cb) => {
|
|
57
|
+
if (typeof chunk === "string")
|
|
58
|
+
return original(toAscii(chunk), encoding, cb);
|
|
59
|
+
return original(chunk, encoding, cb);
|
|
60
|
+
});
|
|
61
|
+
stream[WRAP] = true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Wrap console stdout and stderr on Windows when the code page is not UTF-8.
|
|
65
|
+
* Returns false when no wrap was installed.
|
|
66
|
+
*/
|
|
67
|
+
export function installWindowsConsoleEncoding(deps = {}) {
|
|
68
|
+
if ((deps.platform ?? process.platform) !== "win32")
|
|
69
|
+
return false;
|
|
70
|
+
const stdout = (deps.stdout ?? process.stdout);
|
|
71
|
+
const stderr = (deps.stderr ?? process.stderr);
|
|
72
|
+
if (!stdout.isTTY && !stderr.isTTY)
|
|
73
|
+
return false;
|
|
74
|
+
const codePage = deps.codePage !== undefined ? deps.codePage : (deps.readCodePage ?? readConsoleCodePage)();
|
|
75
|
+
if (codePage === 65001)
|
|
76
|
+
return false;
|
|
77
|
+
if (stdout.isTTY)
|
|
78
|
+
wrapStream(stdout);
|
|
79
|
+
if (stderr.isTTY)
|
|
80
|
+
wrapStream(stderr);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=console-encoding.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare function isInstallFollowupSkipped(argv: readonly string[], env: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
export declare function isInstalledPackageEntry(entry: string | undefined): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* `version`: the CLI version whose mesh daemon install succeeded.
|
|
5
|
+
* `path`: the shim folder and CLI version whose PATH step already ran, so a
|
|
6
|
+
* refused or locked HKCU is not retried on every command.
|
|
7
|
+
* `meshFailedAt`: when a daemon install for `meshFailedVersion` last failed.
|
|
8
|
+
* A failure is retried after MESH_RETRY_MS rather than on every command.
|
|
9
|
+
*/
|
|
10
|
+
export interface InstallFollowupMarker {
|
|
11
|
+
version?: string;
|
|
12
|
+
path?: {
|
|
13
|
+
dir: string;
|
|
14
|
+
version: string;
|
|
15
|
+
};
|
|
16
|
+
meshFailedVersion?: string;
|
|
17
|
+
meshFailedAt?: number;
|
|
18
|
+
}
|
|
19
|
+
/** A failed daemon install blocks for up to 30 s, so it is retried at most hourly. */
|
|
20
|
+
export declare const MESH_RETRY_MS: number;
|
|
21
|
+
export interface InstallFollowupDeps {
|
|
22
|
+
argv: readonly string[];
|
|
23
|
+
env?: NodeJS.ProcessEnv;
|
|
24
|
+
/** The npm shim folder for this entry, or null when hq is not an npm global install. */
|
|
25
|
+
shimDir?: () => string | null;
|
|
26
|
+
ensurePath?: () => void;
|
|
27
|
+
/** True when the daemon install succeeded. */
|
|
28
|
+
spawnMesh?: () => boolean;
|
|
29
|
+
version?: string;
|
|
30
|
+
readMarker?: () => InstallFollowupMarker;
|
|
31
|
+
writeMarker?: (marker: InstallFollowupMarker) => void;
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
export declare function runWindowsInstallFollowup(deps: InstallFollowupDeps): void;
|
|
35
|
+
//# sourceMappingURL=install-followup.d.ts.map
|
|
@@ -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
|