@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,9 @@
|
|
|
1
|
+
export interface ReconcileResult {
|
|
2
|
+
/** Codex user-scope trust keys for the HQ entries in the global hooks.json. */
|
|
3
|
+
codexUserKeys: string[];
|
|
4
|
+
/** One `<file>: <reason>` line per registration file left unchanged by an error. */
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Reconcile existing registrations only; reindex must not install a runtime. */
|
|
8
|
+
export declare function reconcileHookRegistrations(root: string, home?: string, env?: NodeJS.ProcessEnv, daemonInstalled?: boolean): ReconcileResult;
|
|
9
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { writeConfigAtomic, jsonFormat } from '../../commands/mcp-registration.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { addHqHooks, removeHqHooks } from '../install/claude-targets.js';
|
|
6
|
+
import { hookTargetFor } from '../install/hook-target.js';
|
|
7
|
+
import { addCodexHooks, removeCodexHooks, codexHookEntry, codexHookTrustKeys } from '../install/codex-targets.js';
|
|
8
|
+
import { engineArgs, isNativeEntry, registrationEngine } from './registration.js';
|
|
9
|
+
/** The project master-hook command shipped in HQ's .claude/settings.json. */
|
|
10
|
+
function projectClaudeCommand(event) {
|
|
11
|
+
return 'env BASH_ENV=/dev/null bash "$CLAUDE_PROJECT_DIR/.claude/hooks/master-hook.sh" ' + event;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Rewrite matching entries where they sit. Groups, order, foreign entries and
|
|
15
|
+
* extra fields such as timeout are kept, so a flag-on then flag-off pass
|
|
16
|
+
* restores the original file.
|
|
17
|
+
*/
|
|
18
|
+
function rewriteEntries(doc, convert) {
|
|
19
|
+
let changed = false;
|
|
20
|
+
const hooks = {};
|
|
21
|
+
for (const [event, groups] of Object.entries(doc.hooks)) {
|
|
22
|
+
hooks[event] = !Array.isArray(groups) ? groups : groups.map((group) => {
|
|
23
|
+
if (!Array.isArray(group?.hooks))
|
|
24
|
+
return group;
|
|
25
|
+
return { ...group, hooks: group.hooks.map((entry) => {
|
|
26
|
+
const next = entry && typeof entry === 'object' ? convert(entry, event) : undefined;
|
|
27
|
+
if (!next)
|
|
28
|
+
return entry;
|
|
29
|
+
changed = true;
|
|
30
|
+
return next;
|
|
31
|
+
}) };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return changed ? { ...doc, hooks } : doc;
|
|
35
|
+
}
|
|
36
|
+
/** Reconcile existing registrations only; reindex must not install a runtime. */
|
|
37
|
+
export function reconcileHookRegistrations(root, home = homedir(), env = process.env, daemonInstalled) {
|
|
38
|
+
const result = { codexUserKeys: [], failures: [] };
|
|
39
|
+
const engine = registrationEngine(home, env);
|
|
40
|
+
// Flag-off rollback restores the same bash target install would choose (the
|
|
41
|
+
// hqd shim when the daemon serves this root). Resolved only when a
|
|
42
|
+
// registration is rewritten, so the default-off path reads no daemon state.
|
|
43
|
+
let target;
|
|
44
|
+
const globalTarget = () => (target ??= engine ? { engine } : hookTargetFor(root, home, daemonInstalled, env));
|
|
45
|
+
const codexBash = (event) => [codexHookEntry(root, event, 'master').command, codexHookEntry(root, event, 'shim').command];
|
|
46
|
+
for (const [file, runtime, global] of [
|
|
47
|
+
[join(root, '.claude/settings.json'), 'claude', false],
|
|
48
|
+
[join(env.CLAUDE_CONFIG_DIR || join(home, '.claude'), 'settings.json'), 'claude', true],
|
|
49
|
+
[join(env.CODEX_HOME || join(home, '.codex'), 'hooks.json'), 'codex', true],
|
|
50
|
+
[join(root, '.codex/hooks.json'), 'codex', false],
|
|
51
|
+
]) {
|
|
52
|
+
// Each file stands alone: an unreadable or locked project file must not
|
|
53
|
+
// undo or hide the trust keys of a global file already rewritten.
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(file))
|
|
56
|
+
continue;
|
|
57
|
+
// Default-off must not acquire a write lock or create backups for a legacy
|
|
58
|
+
// registration. The merge below repeats this check under the lock when a
|
|
59
|
+
// native entry was found, so a concurrent writer still wins safely.
|
|
60
|
+
if (!engine) {
|
|
61
|
+
let current;
|
|
62
|
+
// A file we cannot parse holds no native entry of ours; leave it untouched.
|
|
63
|
+
try {
|
|
64
|
+
current = jsonFormat.parse(readFileSync(file, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const entries = current?.hooks && typeof current.hooks === 'object'
|
|
70
|
+
? Object.values(current.hooks).flatMap(groups => Array.isArray(groups) ? groups.flatMap(g => Array.isArray(g?.hooks) ? g.hooks : []) : [])
|
|
71
|
+
: [];
|
|
72
|
+
if (!entries.some(e => e && isNativeEntry(e, root)))
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let expected;
|
|
76
|
+
let codexKeys;
|
|
77
|
+
writeConfigAtomic({ target: file, pack: 'hq-hook-engine', format: {
|
|
78
|
+
...jsonFormat,
|
|
79
|
+
assertEntry: (doc) => {
|
|
80
|
+
if (expected !== undefined && JSON.stringify(doc) !== expected)
|
|
81
|
+
throw new Error('Hook registration verification failed');
|
|
82
|
+
},
|
|
83
|
+
}, env: { home }, assertName: 'hooks', merge: (doc) => {
|
|
84
|
+
if (!doc || typeof doc !== 'object' || !doc.hooks || typeof doc.hooks !== 'object')
|
|
85
|
+
return doc;
|
|
86
|
+
const entries = Object.values(doc.hooks).flatMap(groups => Array.isArray(groups) ? groups.flatMap(g => Array.isArray(g?.hooks) ? g.hooks : []) : []);
|
|
87
|
+
if (!engine && !entries.some(e => e && isNativeEntry(e, root)))
|
|
88
|
+
return doc;
|
|
89
|
+
if (global && (runtime === 'codex' ? removeCodexHooks(doc, root) : removeHqHooks(doc, root)) === doc)
|
|
90
|
+
return doc;
|
|
91
|
+
let next;
|
|
92
|
+
if (global)
|
|
93
|
+
next = runtime === 'codex' ? addCodexHooks(doc, root, globalTarget()) : addHqHooks(doc, root, globalTarget());
|
|
94
|
+
else if (runtime === 'codex') {
|
|
95
|
+
// Only an entry install itself writes is converted. HQ's own project
|
|
96
|
+
// file routes through hook-gate.sh and has none, so it stays as is.
|
|
97
|
+
next = rewriteEntries(doc, (entry, event) => {
|
|
98
|
+
if (!isNativeEntry(entry, root) && !codexBash(event).includes(entry.command))
|
|
99
|
+
return undefined;
|
|
100
|
+
const { command: _command, commandWindows: _windows, ...rest } = entry;
|
|
101
|
+
const { command, commandWindows } = codexHookEntry(root, event, globalTarget());
|
|
102
|
+
return { ...rest, command, ...(commandWindows ? { commandWindows } : {}) };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Only the exact shipped master-hook command is converted; a wrapped
|
|
107
|
+
// or extended invocation is someone's edit and is left alone.
|
|
108
|
+
next = rewriteEntries(doc, (entry, event) => {
|
|
109
|
+
if (!isNativeEntry(entry, root) && entry.command !== projectClaudeCommand(event))
|
|
110
|
+
return undefined;
|
|
111
|
+
const { args, commandWindows: _windows, ...rest } = entry;
|
|
112
|
+
if (engine)
|
|
113
|
+
return { ...rest, command: engine, args: engineArgs(root, event, 'claude') };
|
|
114
|
+
return { ...rest, command: projectClaudeCommand(Array.isArray(args) && typeof args[0] === 'string' ? args[0] : event) };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
expected = JSON.stringify(next);
|
|
118
|
+
if (runtime === 'codex' && global)
|
|
119
|
+
codexKeys = codexHookTrustKeys(next, file, root);
|
|
120
|
+
return next;
|
|
121
|
+
} });
|
|
122
|
+
// Keys are positions in the file. Only once the write and its verify have
|
|
123
|
+
// succeeded do they point at HQ's entries; a failed write leaves the old
|
|
124
|
+
// file, where the same position can hold a foreign hook.
|
|
125
|
+
if (codexKeys)
|
|
126
|
+
result.codexUserKeys = codexKeys;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
result.failures.push(`${file}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function launcherEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
export declare function registrationEngine(home: string, env?: NodeJS.ProcessEnv): string | null;
|
|
3
|
+
export interface NativeTarget {
|
|
4
|
+
engine: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Root spelling passed to the launcher. A trailing separator is dropped because
|
|
8
|
+
* Windows PowerShell 5.1 re-quotes a native argument containing a space as
|
|
9
|
+
* "...", and a final backslash would then escape the closing quote.
|
|
10
|
+
*/
|
|
11
|
+
export declare function launcherRoot(root: string): string;
|
|
12
|
+
export declare function engineArgs(root: string, event: string, runtime: 'claude' | 'codex', global?: boolean): string[];
|
|
13
|
+
/** Recognize only our pinned executable with the exact root argument. */
|
|
14
|
+
export declare function isNativeEntry(entry: Record<string, unknown>, root: string): boolean;
|
|
15
|
+
export declare function quotePosix(value: string): string;
|
|
16
|
+
export declare function quoteWindows(value: string): string;
|
|
17
|
+
//# sourceMappingURL=registration.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Offline, default-off registration gate. No network work on the hook path. */
|
|
2
|
+
import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from '../flag-snapshot-file.js';
|
|
3
|
+
import { resolveProcessFlagCompanyContext } from '../process-flag-company-context.js';
|
|
4
|
+
import { installHookEngine } from './resolve.js';
|
|
5
|
+
export function launcherEnabled(env = process.env) {
|
|
6
|
+
const { companyUid: uid, companySlug } = resolveProcessFlagCompanyContext(env);
|
|
7
|
+
if (!uid && companySlug)
|
|
8
|
+
return false;
|
|
9
|
+
const snapshot = uid ? readFlagSnapshotFile(uid, { env }) : readPersonFlagSnapshotFile({ env });
|
|
10
|
+
return snapshot?.flags['hook-engine-launcher'] === true;
|
|
11
|
+
}
|
|
12
|
+
export function registrationEngine(home, env = process.env) {
|
|
13
|
+
if (!launcherEnabled({ ...env, HOME: home, USERPROFILE: home }))
|
|
14
|
+
return null;
|
|
15
|
+
return installHookEngine({ home });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Root spelling passed to the launcher. A trailing separator is dropped because
|
|
19
|
+
* Windows PowerShell 5.1 re-quotes a native argument containing a space as
|
|
20
|
+
* "...", and a final backslash would then escape the closing quote.
|
|
21
|
+
*/
|
|
22
|
+
export function launcherRoot(root) {
|
|
23
|
+
const trimmed = root.replace(/[\\/]+$/, '');
|
|
24
|
+
return trimmed === '' || /^[A-Za-z]:$/.test(trimmed) ? root : trimmed;
|
|
25
|
+
}
|
|
26
|
+
export function engineArgs(root, event, runtime, global = false) {
|
|
27
|
+
return [event, '--hq-root', launcherRoot(root), '--runtime', runtime, ...(global && runtime === 'claude' ? ['--global'] : [])];
|
|
28
|
+
}
|
|
29
|
+
/** Recognize only our pinned executable with the exact root argument. */
|
|
30
|
+
export function isNativeEntry(entry, root) {
|
|
31
|
+
if (typeof entry.command !== 'string')
|
|
32
|
+
return false;
|
|
33
|
+
const pinned = /[\\/]\.hq[\\/]bin[\\/]hq-hook[\\/]\d+\.\d+\.\d+[^\\/]*[\\/]/;
|
|
34
|
+
if (!pinned.test(entry.command))
|
|
35
|
+
return false;
|
|
36
|
+
const spelling = launcherRoot(root);
|
|
37
|
+
if (Array.isArray(entry.args)) {
|
|
38
|
+
const i = entry.args.indexOf('--hq-root');
|
|
39
|
+
return i >= 0 && entry.args[i + 1] === spelling;
|
|
40
|
+
}
|
|
41
|
+
return entry.command.includes(quotePosix('--hq-root') + ' ' + quotePosix(spelling));
|
|
42
|
+
}
|
|
43
|
+
export function quotePosix(value) { return "'" + value.replace(/'/g, "'\\''") + "'"; }
|
|
44
|
+
export function quoteWindows(value) {
|
|
45
|
+
// Codex 0.162.1 uses its Windows PowerShell runtime shell for hooks.
|
|
46
|
+
// Single-quoted literals prevent variable, subexpression and wildcard expansion.
|
|
47
|
+
// PowerShell also closes them on the typographic quotes U+2018-U+201B.
|
|
48
|
+
return "'" + value.replace(/['‘’‚‛]/g, '$&$&') + "'";
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=registration.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NativeTarget } from '../hook-engine/registration.js';
|
|
1
2
|
/** Name of the HQ MCP server entry in ~/.claude.json. */
|
|
2
3
|
export declare const HQ_MCP_SERVER_NAME = "hq";
|
|
3
4
|
/** `_hqPack` provenance stamped on the HQ MCP server entry. */
|
|
@@ -21,7 +22,7 @@ export declare function masterHookPath(hqRoot: string): string;
|
|
|
21
22
|
/** hqd hook shim (US-010): registered instead of master-hook.sh once hqd is installed. */
|
|
22
23
|
export declare function shimPath(hqRoot: string): string;
|
|
23
24
|
/** Which HQ hook script the user-scope install registers. */
|
|
24
|
-
export type HookTarget = 'master' | 'shim';
|
|
25
|
+
export type HookTarget = 'master' | 'shim' | NativeTarget;
|
|
25
26
|
export declare function charterPath(hqRoot: string): string;
|
|
26
27
|
export declare function hqSkillsSource(hqRoot: string): string;
|
|
27
28
|
/** The exact hook command registered for one event (absolute path, quoted). */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* safe-write substrate.
|
|
6
6
|
*/
|
|
7
7
|
import * as path from 'path';
|
|
8
|
+
import { engineArgs, isNativeEntry } from '../hook-engine/registration.js';
|
|
8
9
|
/** Events master-hook.sh is registered for at user scope. */
|
|
9
10
|
const GLOBAL_HOOK_EVENTS = [
|
|
10
11
|
'SessionStart',
|
|
@@ -98,6 +99,8 @@ export function removeClaudeMdBlock(current) {
|
|
|
98
99
|
function isHqHookEntry(entry, hqRoot) {
|
|
99
100
|
if (!entry || typeof entry !== 'object')
|
|
100
101
|
return false;
|
|
102
|
+
if (isNativeEntry(entry, hqRoot))
|
|
103
|
+
return true;
|
|
101
104
|
const cmd = entry.command;
|
|
102
105
|
if (typeof cmd !== 'string')
|
|
103
106
|
return false;
|
|
@@ -111,13 +114,15 @@ export function addHqHooks(doc, hqRoot, target = 'master') {
|
|
|
111
114
|
const hooks = { ...(doc.hooks ?? {}) };
|
|
112
115
|
let changed = false;
|
|
113
116
|
for (const event of GLOBAL_HOOK_EVENTS) {
|
|
114
|
-
const want =
|
|
117
|
+
const want = typeof target === 'object'
|
|
118
|
+
? { type: 'command', command: target.engine, args: engineArgs(hqRoot, event, 'claude', true), timeout: 300 }
|
|
119
|
+
: { type: 'command', command: hookCommand(hqRoot, event, target), timeout: 300 };
|
|
115
120
|
let groups = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
116
|
-
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 && JSON.stringify(h.args) === JSON.stringify('args' in want ? want.args : undefined)));
|
|
117
122
|
if (present)
|
|
118
123
|
continue;
|
|
119
124
|
groups = removeHqHooks({ hooks: { [event]: groups } }, hqRoot).hooks?.[event] ?? [];
|
|
120
|
-
groups.push({ hooks: [
|
|
125
|
+
groups.push({ hooks: [want] });
|
|
121
126
|
hooks[event] = groups;
|
|
122
127
|
changed = true;
|
|
123
128
|
}
|
|
@@ -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
|
|
@@ -20,7 +20,7 @@ export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman,
|
|
|
20
20
|
export type { LaneStatusSnapshot, ProjectStoryLedger, StoryLedgerWriteDeps } from "./story-ledger.js";
|
|
21
21
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
22
22
|
export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
23
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
23
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, expandLocalLaneIdPrefix, LANE_ID_PREFIX_MIN, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
24
24
|
export type { StoreDeps } from "./store.js";
|
|
25
25
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
26
26
|
export type { LaneWatchDeps, LaneWatchResult } from "./watch.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFT
|
|
|
12
12
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
13
13
|
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
14
14
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
15
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
15
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, expandLocalLaneIdPrefix, LANE_ID_PREFIX_MIN, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readAllLanes, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
16
16
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
17
17
|
export { MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_TASK_OUTPUT_MAX_BYTES, MONITOR_TRANSCRIPT_MAX_BYTES, MONITOR_UNCOVERED_EXIT_CODE, coveredLaneIds, isMonitorTaskLive, monitorReminderText, parseCanonicalWatchCommand, parseMonitorTranscript, readLiveMonitorEvidence, } from "./monitor.js";
|
|
18
18
|
export { IMDS_HOST, IMDS_INSTANCE_ID_URL, IMDS_TIMEOUT_MS, IMDS_TOKEN_URL, defaultReadImdsInstanceId, resolveMachineIdentity, } from "./machine-identity.js";
|
|
@@ -87,5 +87,16 @@ export declare function createQuestion(hqRoot: string, init: Omit<QuestionRecord
|
|
|
87
87
|
export declare function updateQuestion(hqRoot: string, questionId: string, expectedVersion: number, updater: (current: QuestionRecord) => QuestionRecord, deps?: StoreDeps): QuestionRecord;
|
|
88
88
|
export declare function appendQuestionRouting(hqRoot: string, questionId: string, expectedVersion: number, entry: RoutingHistoryEntry, deps?: StoreDeps): QuestionRecord;
|
|
89
89
|
export declare function listLaneIds(hqRoot: string, deps?: StoreDeps): string[];
|
|
90
|
+
/** Shortest lane-id prefix a command expands to a full local lane id. */
|
|
91
|
+
export declare const LANE_ID_PREFIX_MIN = 8;
|
|
92
|
+
/**
|
|
93
|
+
* Expand a short lane-id prefix (at least `LANE_ID_PREFIX_MIN` ULID chars,
|
|
94
|
+
* no host suffix) to the one local lane id it starts. Returns `undefined`
|
|
95
|
+
* when the input is not such a prefix or matches no local lane, and throws
|
|
96
|
+
* `lane_id_ambiguous` listing every candidate when it matches more than one.
|
|
97
|
+
* Lane records, questions, run dirs and envelopes are always keyed by the
|
|
98
|
+
* full lane id; a prefix is only ever a lookup convenience.
|
|
99
|
+
*/
|
|
100
|
+
export declare function expandLocalLaneIdPrefix(hqRoot: string, input: string, deps?: StoreDeps): string | undefined;
|
|
90
101
|
export declare function listQuestionIds(hqRoot: string, deps?: StoreDeps): string[];
|
|
91
102
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -2072,6 +2072,26 @@ function listRecordIds(dir, label, deps) {
|
|
|
2072
2072
|
export function listLaneIds(hqRoot, deps = defaultStoreDeps()) {
|
|
2073
2073
|
return listRecordIds(path.join(lanesStoreRoot(hqRoot), LANE_RECORDS_DIR), "lane", deps);
|
|
2074
2074
|
}
|
|
2075
|
+
/** Shortest lane-id prefix a command expands to a full local lane id. */
|
|
2076
|
+
export const LANE_ID_PREFIX_MIN = 8;
|
|
2077
|
+
/**
|
|
2078
|
+
* Expand a short lane-id prefix (at least `LANE_ID_PREFIX_MIN` ULID chars,
|
|
2079
|
+
* no host suffix) to the one local lane id it starts. Returns `undefined`
|
|
2080
|
+
* when the input is not such a prefix or matches no local lane, and throws
|
|
2081
|
+
* `lane_id_ambiguous` listing every candidate when it matches more than one.
|
|
2082
|
+
* Lane records, questions, run dirs and envelopes are always keyed by the
|
|
2083
|
+
* full lane id; a prefix is only ever a lookup convenience.
|
|
2084
|
+
*/
|
|
2085
|
+
export function expandLocalLaneIdPrefix(hqRoot, input, deps = defaultStoreDeps()) {
|
|
2086
|
+
if (!new RegExp(`^[0-9A-HJKMNP-TV-Z]{${LANE_ID_PREFIX_MIN},26}$`, "i").test(input))
|
|
2087
|
+
return undefined;
|
|
2088
|
+
const prefix = input.toUpperCase();
|
|
2089
|
+
const matches = listLaneIds(hqRoot, deps).filter((laneId) => laneId.toUpperCase().startsWith(prefix)).sort();
|
|
2090
|
+
if (matches.length > 1) {
|
|
2091
|
+
throw new LanesError("lane_id_ambiguous", `Lane id prefix ${input} matches ${matches.length} lanes: ${matches.join(", ")}. Pass the full lane id.`, { lane_id: input, matches });
|
|
2092
|
+
}
|
|
2093
|
+
return matches[0];
|
|
2094
|
+
}
|
|
2075
2095
|
export function listQuestionIds(hqRoot, deps = defaultStoreDeps()) {
|
|
2076
2096
|
return listRecordIds(path.join(lanesStoreRoot(hqRoot), QUESTION_RECORDS_DIR), "question", deps);
|
|
2077
2097
|
}
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
1
|
+
import { expandLocalLaneIdPrefix, listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
2
|
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
3
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
4
4
|
import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
|
|
@@ -62,7 +62,9 @@ function canResumeKeepAliveLane(hqRoot, lane, nowMs = Date.now()) {
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
65
|
-
|
|
65
|
+
// Watch by full lane id: a short prefix expands only when it names exactly
|
|
66
|
+
// one local lane, and an ambiguous prefix is refused with its candidates.
|
|
67
|
+
const watchedIds = [...new Set(validateLaneIds(laneIds).map((laneId) => expandLocalLaneIdPrefix(hqRoot, laneId) ?? laneId))];
|
|
66
68
|
const readLaneRecord = deps.readLane ?? readLane;
|
|
67
69
|
const listQuestions = deps.listQuestionIds ?? listQuestionIds;
|
|
68
70
|
const readQuestionRecord = deps.readQuestion ?? readQuestion;
|
|
@@ -130,7 +132,7 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
130
132
|
}
|
|
131
133
|
records.push(record);
|
|
132
134
|
if (previousStates.get(laneId) !== record.state) {
|
|
133
|
-
emit(`lane ${
|
|
135
|
+
emit(`lane ${record.lane_id}: state=${record.state}${isTerminalLaneState(record.state) ? " (terminal)" : ""}`);
|
|
134
136
|
previousStates.set(laneId, record.state);
|
|
135
137
|
}
|
|
136
138
|
}
|
|
@@ -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
|