@indigoai-us/hq-cli 5.345.147 → 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.
Files changed (168) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +341 -11
  5. package/dist/command-catalog.generated.js +421 -11
  6. package/dist/command-help.js +2 -1
  7. package/dist/commands/access.d.ts +45 -2
  8. package/dist/commands/access.js +537 -223
  9. package/dist/commands/agent-inbox.js +2 -1
  10. package/dist/commands/billing-usage.d.ts +33 -2
  11. package/dist/commands/billing-usage.js +75 -4
  12. package/dist/commands/bot.js +2 -2
  13. package/dist/commands/channels.d.ts +27 -0
  14. package/dist/commands/channels.js +108 -4
  15. package/dist/commands/cloud.js +31 -1
  16. package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
  17. package/dist/commands/core-refresh-vault-access-native.js +356 -28
  18. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  19. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  20. package/dist/commands/core.js +9 -0
  21. package/dist/commands/daemon.d.ts +7 -0
  22. package/dist/commands/daemon.js +37 -3
  23. package/dist/commands/dm.js +18 -3
  24. package/dist/commands/doctor.d.ts +11 -1
  25. package/dist/commands/doctor.js +56 -1
  26. package/dist/commands/install-global.d.ts +8 -8
  27. package/dist/commands/install-global.js +139 -61
  28. package/dist/commands/lanes-shared.d.ts +3 -1
  29. package/dist/commands/lanes.d.ts +4 -4
  30. package/dist/commands/lanes.js +380 -25
  31. package/dist/commands/library-add.d.ts +31 -1
  32. package/dist/commands/library-add.js +69 -2
  33. package/dist/commands/library.js +37 -3
  34. package/dist/commands/pack-install.d.ts +2 -2
  35. package/dist/commands/pack-install.js +133 -35
  36. package/dist/commands/reindex.js +19 -0
  37. package/dist/commands/sync-manifest.d.ts +2 -9
  38. package/dist/commands/sync-manifest.js +4 -15
  39. package/dist/hook-engine-path.d.ts +4 -0
  40. package/dist/hook-engine-path.js +43 -0
  41. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  42. package/dist/hq-cloud-sync-runner/package.json +1 -1
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +76 -5
  45. package/dist/lazy-commands.js +2 -0
  46. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  47. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  48. package/dist/lib/agent-kit/inbox-state.js +11 -2
  49. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  50. package/dist/lib/agent-kit/run/inbox.js +36 -7
  51. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  52. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  53. package/dist/lib/billing/usage-cost.d.ts +31 -1
  54. package/dist/lib/billing/usage-cost.js +46 -0
  55. package/dist/lib/company-skill-wrapper.js +6 -4
  56. package/dist/lib/daemon/install.d.ts +47 -1
  57. package/dist/lib/daemon/install.js +181 -15
  58. package/dist/lib/daemon/lock.d.ts +32 -0
  59. package/dist/lib/daemon/lock.js +55 -2
  60. package/dist/lib/db/local.d.ts +2 -2
  61. package/dist/lib/db/local.js +2 -2
  62. package/dist/lib/db/sql.js +2 -2
  63. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  64. package/dist/lib/db/sqlite-binding.js +168 -0
  65. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  66. package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
  67. package/dist/lib/doctor/checks/anywhere.js +77 -11
  68. package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
  69. package/dist/lib/doctor/checks/cli-path.js +46 -0
  70. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  71. package/dist/lib/doctor/checks/hook-engine.js +17 -0
  72. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  73. package/dist/lib/doctor/checks/sync-health.js +38 -2
  74. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  75. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  76. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  77. package/dist/lib/doctor/fix/apply.js +176 -10
  78. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  79. package/dist/lib/doctor/fix/json-output.js +6 -0
  80. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  81. package/dist/lib/doctor/fix/remediation.js +23 -1
  82. package/dist/lib/doctor/registry.js +10 -0
  83. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  84. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  85. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  86. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  87. package/dist/lib/flag-registry.js +9 -1
  88. package/dist/lib/flag-snapshot-file.js +8 -3
  89. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  90. package/dist/lib/hook-engine/reconcile.js +134 -0
  91. package/dist/lib/hook-engine/registration.d.ts +17 -0
  92. package/dist/lib/hook-engine/registration.js +50 -0
  93. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  94. package/dist/lib/hook-engine/resolve.js +134 -0
  95. package/dist/lib/install/claude-targets.d.ts +2 -1
  96. package/dist/lib/install/claude-targets.js +8 -3
  97. package/dist/lib/install/codex-targets.d.ts +3 -0
  98. package/dist/lib/install/codex-targets.js +15 -4
  99. package/dist/lib/install/hook-target.d.ts +9 -0
  100. package/dist/lib/install/hook-target.js +39 -0
  101. package/dist/lib/lanes/admission-host.d.ts +1 -0
  102. package/dist/lib/lanes/admission-host.js +8 -1
  103. package/dist/lib/lanes/admission.d.ts +4 -0
  104. package/dist/lib/lanes/admission.js +6 -2
  105. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  107. package/dist/lib/lanes/index.d.ts +2 -2
  108. package/dist/lib/lanes/index.js +1 -1
  109. package/dist/lib/lanes/keep-alive.js +4 -1
  110. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  111. package/dist/lib/lanes/local-host-identity.js +314 -0
  112. package/dist/lib/lanes/reaper.d.ts +1 -1
  113. package/dist/lib/lanes/reaper.js +7 -0
  114. package/dist/lib/lanes/reconcile.d.ts +4 -0
  115. package/dist/lib/lanes/reconcile.js +16 -8
  116. package/dist/lib/lanes/spawn.d.ts +20 -0
  117. package/dist/lib/lanes/spawn.js +48 -2
  118. package/dist/lib/lanes/status-fields.js +6 -1
  119. package/dist/lib/lanes/store.js +8 -1
  120. package/dist/lib/lanes/throttle.d.ts +49 -6
  121. package/dist/lib/lanes/throttle.js +154 -17
  122. package/dist/lib/lanes/types.d.ts +22 -1
  123. package/dist/lib/lanes/types.js +27 -3
  124. package/dist/lib/lanes/watcher.js +27 -2
  125. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  126. package/dist/lib/lanes/worker-continuity.js +18 -11
  127. package/dist/lib/manifest-disabled.d.ts +13 -0
  128. package/dist/lib/manifest-disabled.js +19 -0
  129. package/dist/lib/service-manager/launchd.d.ts +1 -0
  130. package/dist/lib/service-manager/launchd.js +1 -1
  131. package/dist/lib/windows/console-encoding.d.ts +24 -0
  132. package/dist/lib/windows/console-encoding.js +83 -0
  133. package/dist/lib/windows/install-followup.d.ts +35 -0
  134. package/dist/lib/windows/install-followup.js +142 -0
  135. package/dist/lib/windows/user-path.d.ts +125 -0
  136. package/dist/lib/windows/user-path.js +275 -0
  137. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  138. package/dist/lib/work-context/auto-bind.js +22 -3
  139. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  140. package/dist/lib/work-context/lane-bind.js +101 -0
  141. package/dist/lib/work-context/organize.js +1 -0
  142. package/dist/lib/work-context/prd-sync.js +7 -0
  143. package/dist/lib/work-context/project.d.ts +36 -10
  144. package/dist/lib/work-context/project.js +130 -41
  145. package/dist/lib/work-context/reconcile.d.ts +6 -0
  146. package/dist/lib/work-context/reconcile.js +25 -5
  147. package/dist/lib/work-context/state.d.ts +6 -0
  148. package/dist/lib/work-context/state.js +29 -1
  149. package/dist/main.js +2 -1
  150. package/dist/unknown-command.d.ts +18 -0
  151. package/dist/unknown-command.js +88 -0
  152. package/dist/utils/cognito-session.d.ts +8 -0
  153. package/dist/utils/cognito-session.js +14 -2
  154. package/dist/utils/copy-tree.d.ts +61 -0
  155. package/dist/utils/copy-tree.js +249 -0
  156. package/dist/utils/environmental-error.js +7 -3
  157. package/dist/utils/hook-trust.d.ts +9 -0
  158. package/dist/utils/hook-trust.js +30 -2
  159. package/dist/utils/npm-invocation.d.ts +27 -0
  160. package/dist/utils/npm-invocation.js +54 -0
  161. package/dist/utils/qmd-native-binding-error.js +3 -1
  162. package/dist/utils/resolve-executable.d.ts +2 -0
  163. package/dist/utils/resolve-executable.js +6 -2
  164. package/dist/utils/safe-link.d.ts +50 -0
  165. package/dist/utils/safe-link.js +300 -0
  166. package/package.json +17 -5
  167. package/scripts/hook-engine-manifest.mjs +22 -0
  168. package/scripts/install-hook-engine.mjs +10 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * copy-tree: portable replacement for the `rsync -a` copies on the pack
3
+ * install path.
4
+ *
5
+ * Pack installs used to shell out to `rsync`, which Windows does not ship, so
6
+ * every git-subpath or local-directory install failed there with
7
+ * `spawnSync rsync ENOENT`. This module does the same copy in Node on every
8
+ * platform:
9
+ *
10
+ * - `copyTree(src, dest)` copies the CONTENTS of `src` into `dest` (the
11
+ * `rsync -a src/ dest/` trailing-slash form). Directories recurse, regular
12
+ * files keep their permission bits and mtimes, and symlinks are recreated
13
+ * as symlinks with the identical target string. Links are never followed.
14
+ * Optional `exclude` names match an entry's basename at any depth, like
15
+ * `rsync --exclude=<name>`. Sockets, FIFOs and devices are skipped with a
16
+ * warning, as a non-root `rsync -a` does.
17
+ * - `removeTreeNoFollow(target)` deletes a tree without ever following a
18
+ * symlink or junction inside it: each entry is lstat'd, links are removed
19
+ * as links (unlink, or a non-recursive rmdir for a Windows junction), and
20
+ * directories are emptied then removed with a non-recursive rmdir. A
21
+ * Windows directory that is a reparse point but not a link (a volume mount
22
+ * point, a cloud-synced placeholder folder) stops the removal with
23
+ * `ReparsePointError`; its contents are never deleted.
24
+ *
25
+ * On Windows, creating a symlink needs Developer Mode or an elevated shell.
26
+ * When that fails with EPERM/EACCES and the link points at a directory inside
27
+ * the tree being copied, the copy falls back to a junction (no privilege
28
+ * needed). A junction needs an absolute target, so it points at the matching
29
+ * entry under `finalRoot`: the path the copy will live at once the caller has
30
+ * renamed it into place. Any other link target throws `CopyTreeLinkError` with
31
+ * a Developer Mode hint instead of silently dropping the link.
32
+ *
33
+ * TODO(windows-links): src/utils/safe-link.ts (PR 1967, branch
34
+ * fix/windows-install-path-daemon) adds its own junction fallback and
35
+ * link-only removal. Whichever of the two PRs merges second should reconcile
36
+ * these helpers into one module.
37
+ */
38
+ import * as fs from 'fs';
39
+ import * as path from 'path';
40
+ export class CopyTreeLinkError extends Error {
41
+ constructor(linkPath, target, cause) {
42
+ super(`Could not create the symlink ${linkPath} -> ${target} (${cause.code ?? cause.message}). ` +
43
+ 'Windows only lets elevated shells create symlinks unless Developer Mode is on. ' +
44
+ 'Turn on Developer Mode (Settings > System > For developers) or run the install from an elevated terminal, then retry.');
45
+ this.name = 'CopyTreeLinkError';
46
+ }
47
+ }
48
+ export class ReparsePointError extends Error {
49
+ constructor(dir) {
50
+ super(`Refusing to delete ${dir}: it is a Windows reparse point (such as a volume mount point or a cloud-synced ` +
51
+ 'placeholder folder), not a plain folder or link. Deleting through it could remove files stored elsewhere. ' +
52
+ 'Unmount or remove it by hand, then retry.');
53
+ this.name = 'ReparsePointError';
54
+ }
55
+ }
56
+ export function copyTree(src, dest, opts = {}) {
57
+ const exclude = new Set(opts.exclude ?? []);
58
+ const srcRoot = path.resolve(src);
59
+ const destRoot = path.resolve(dest);
60
+ const finalRoot = path.resolve(opts.finalRoot ?? dest);
61
+ const rootStat = fs.statSync(srcRoot);
62
+ if (!rootStat.isDirectory()) {
63
+ throw new Error(`copyTree source is not a directory: ${srcRoot}`);
64
+ }
65
+ fs.mkdirSync(destRoot, { recursive: true });
66
+ copyDirContents(srcRoot, destRoot, { srcRoot, finalRoot, exclude });
67
+ applyMetadata(destRoot, rootStat);
68
+ }
69
+ function copyDirContents(srcDir, destDir, ctx) {
70
+ for (const name of fs.readdirSync(srcDir)) {
71
+ if (ctx.exclude.has(name))
72
+ continue;
73
+ const srcPath = path.join(srcDir, name);
74
+ const destPath = path.join(destDir, name);
75
+ const st = fs.lstatSync(srcPath);
76
+ if (st.isSymbolicLink()) {
77
+ copyLink(srcPath, destPath, ctx);
78
+ }
79
+ else if (st.isDirectory()) {
80
+ fs.mkdirSync(destPath, { recursive: true });
81
+ copyDirContents(srcPath, destPath, ctx);
82
+ // Children are written first, so the directory's own mtime and mode are
83
+ // set last and stay equal to the source.
84
+ applyMetadata(destPath, st);
85
+ }
86
+ else if (st.isFile()) {
87
+ fs.copyFileSync(srcPath, destPath);
88
+ applyMetadata(destPath, st);
89
+ }
90
+ else {
91
+ process.stderr.write(`hq: skipping special file ${srcPath} (not a file, directory or symlink)\n`);
92
+ }
93
+ }
94
+ }
95
+ function applyMetadata(target, st) {
96
+ fs.chmodSync(target, st.mode & 0o7777);
97
+ fs.utimesSync(target, st.atime, st.mtime);
98
+ }
99
+ function copyLink(srcPath, destPath, ctx) {
100
+ const target = fs.readlinkSync(srcPath);
101
+ const resolved = path.resolve(path.dirname(srcPath), target);
102
+ const targetIsDir = isDirectory(resolved);
103
+ try {
104
+ fs.symlinkSync(target, destPath, targetIsDir ? 'dir' : 'file');
105
+ }
106
+ catch (err) {
107
+ const e = err;
108
+ if (process.platform !== 'win32' || (e.code !== 'EPERM' && e.code !== 'EACCES'))
109
+ throw e;
110
+ if (!targetIsDir || !isInside(ctx.srcRoot, resolved)) {
111
+ throw new CopyTreeLinkError(destPath, target, e);
112
+ }
113
+ // A junction needs an absolute target. Point it at the same directory in
114
+ // the copy as it will be after the caller's final rename, so the junction
115
+ // stays inside the installed tree and does not dangle once a staging dir
116
+ // is renamed. Windows creates a junction to a path that does not exist
117
+ // yet, so nothing is created here: the target appears when the copy
118
+ // reaches it, and a target under an excluded name stays absent, as the
119
+ // link would dangle after `rsync --exclude`.
120
+ const junctionTarget = path.join(ctx.finalRoot, path.relative(ctx.srcRoot, resolved));
121
+ fs.symlinkSync(junctionTarget, destPath, 'junction');
122
+ }
123
+ }
124
+ function isDirectory(p) {
125
+ try {
126
+ return fs.statSync(p).isDirectory();
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ function isInside(root, candidate) {
133
+ const rel = path.relative(root, candidate);
134
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
135
+ }
136
+ /**
137
+ * Remove `target` and everything under it without following links. A missing
138
+ * target is not an error. If `target` itself is a link, only the link goes.
139
+ */
140
+ export function removeTreeNoFollow(target) {
141
+ removeEntry(target, () => listedAsLink(target));
142
+ }
143
+ /**
144
+ * `listedAsLink` reports whether the directory listing marks the entry as a
145
+ * link. On Windows Node marks every entry with FILE_ATTRIBUTE_REPARSE_POINT
146
+ * that way, while lstat reports only symlinks and junctions as links. A
147
+ * directory that lstat calls a directory but the listing calls a link is a
148
+ * reparse point of another kind, such as a volume mount point or a cloud
149
+ * placeholder. Elsewhere the two always agree.
150
+ */
151
+ function removeEntry(target, listedAsLink) {
152
+ let st;
153
+ try {
154
+ st = fs.lstatSync(target);
155
+ }
156
+ catch (err) {
157
+ if (err.code === 'ENOENT')
158
+ return;
159
+ throw err;
160
+ }
161
+ if (st.isSymbolicLink()) {
162
+ removeLinkOnly(target);
163
+ return;
164
+ }
165
+ if (st.isDirectory()) {
166
+ if (listedAsLink())
167
+ throw new ReparsePointError(target);
168
+ for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
169
+ removeEntry(path.join(target, entry.name), () => entry.isSymbolicLink());
170
+ }
171
+ fs.rmdirSync(target);
172
+ return;
173
+ }
174
+ try {
175
+ fs.unlinkSync(target);
176
+ }
177
+ catch (err) {
178
+ const code = err.code;
179
+ if (code !== 'EPERM' && code !== 'EACCES')
180
+ throw err;
181
+ if (process.platform === 'win32') {
182
+ // Windows refuses to delete a file with the read-only attribute, which a
183
+ // copied 0o444 file carries. Clear it on the file itself (not a link,
184
+ // checked above) and retry once.
185
+ retryWithMode(target, 0o666, () => fs.unlinkSync(target));
186
+ }
187
+ else {
188
+ // On POSIX the right to unlink comes from the parent folder, which a
189
+ // copied 0o555 folder lacks. Give its owner write access and retry once.
190
+ const parent = path.dirname(target);
191
+ retryWithMode(parent, (fs.statSync(parent).mode & 0o7777) | 0o700, () => fs.unlinkSync(target), true);
192
+ }
193
+ }
194
+ }
195
+ /**
196
+ * Set `p` to `mode`, run `op` once, and put the original mode back if `op`
197
+ * throws, or always when `alwaysRestore` is set.
198
+ */
199
+ function retryWithMode(p, mode, op, alwaysRestore = false) {
200
+ const original = fs.statSync(p).mode & 0o7777;
201
+ fs.chmodSync(p, mode);
202
+ let ok = false;
203
+ try {
204
+ op();
205
+ ok = true;
206
+ }
207
+ finally {
208
+ if (!ok || alwaysRestore) {
209
+ try {
210
+ fs.chmodSync(p, original);
211
+ }
212
+ catch {
213
+ // The retry's own error, or its success, is what the caller needs.
214
+ }
215
+ }
216
+ }
217
+ }
218
+ function listedAsLink(target) {
219
+ const parent = path.dirname(target);
220
+ if (parent === target)
221
+ return false;
222
+ const name = path.basename(target);
223
+ let entries;
224
+ try {
225
+ entries = fs.readdirSync(parent, { withFileTypes: true });
226
+ }
227
+ catch (err) {
228
+ // A parent that cannot be listed gives no listing to compare against.
229
+ // Each child is still checked against its own listing during the walk.
230
+ if (err.code === 'EACCES' || err.code === 'EPERM')
231
+ return false;
232
+ throw err;
233
+ }
234
+ return entries.some((e) => e.name === name && e.isSymbolicLink());
235
+ }
236
+ function removeLinkOnly(linkPath) {
237
+ try {
238
+ fs.unlinkSync(linkPath);
239
+ }
240
+ catch (err) {
241
+ const code = err.code;
242
+ // Windows directory symlinks and junctions can refuse unlink. A
243
+ // non-recursive rmdir removes the link entry and never touches the target.
244
+ if (code !== 'EPERM' && code !== 'EISDIR' && code !== 'EACCES')
245
+ throw err;
246
+ fs.rmdirSync(linkPath);
247
+ }
248
+ }
249
+ //# sourceMappingURL=copy-tree.js.map
@@ -262,9 +262,13 @@ export function filesystemErrorMessage(err) {
262
262
  const safePath = redactedPath(details.rawPath);
263
263
  const synced = syncedFolder(resolvedPath);
264
264
  const permissionDenied = details.code === "EACCES" || details.code === "EPERM";
265
- const remedy = permissionDenied && process.platform !== "win32" && synced === "none"
266
- ? "Check file and folder permissions, close the file, then retry."
267
- : "Close the file or exclude its folder from cloud sync; if it persists, check permissions, then retry.";
265
+ // Windows refuses symlinks without SeCreateSymbolicLinkPrivilege; that is a
266
+ // privilege problem, not a locked or cloud-synced file.
267
+ const remedy = permissionDenied && process.platform === "win32" && details.syscall === "symlink"
268
+ ? "Windows needs Developer Mode (Settings > System > For developers) or an elevated terminal to create links; turn one on, then retry."
269
+ : permissionDenied && process.platform !== "win32" && synced === "none"
270
+ ? "Check file and folder permissions, close the file, then retry."
271
+ : "Close the file or exclude its folder from cloud sync; if it persists, check permissions, then retry.";
268
272
  return `Filesystem operation ${details.syscall} failed (${details.code}) at ${safePath}. ${remedy}`;
269
273
  }
270
274
  //# sourceMappingURL=environmental-error.js.map
@@ -1,3 +1,5 @@
1
+ import { findExecutableOnPath } from './resolve-executable.js';
2
+ import { type SpawnPlan } from './windows-spawn.js';
1
3
  export interface CodexRpcClient {
2
4
  request(method: string, params: unknown): Promise<unknown>;
3
5
  close(): Promise<void>;
@@ -49,6 +51,13 @@ export interface RuntimeHookTrustResult {
49
51
  trusted: number;
50
52
  reason?: string;
51
53
  }
54
+ /**
55
+ * How to launch Codex. npm installs Codex on Windows as a `codex.cmd` shim,
56
+ * which `spawn('codex')` cannot find (ENOENT) and Node refuses to run without a
57
+ * shell, so a bare name is resolved through PATH + PATHEXT and a batch shim is
58
+ * routed through the escaped cmd.exe plan.
59
+ */
60
+ export declare function planCodexLaunch(executable: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, find?: typeof findExecutableOnPath): SpawnPlan;
52
61
  /** Start a short-lived Codex app-server and complete its JSONL handshake. */
53
62
  export declare function createCodexAppServerClient(cwd: string, executable?: string, args?: string[]): Promise<CodexRpcClient>;
54
63
  /** Trust only hooks declared by this HQ root's project `.codex/` layer. */
@@ -8,6 +8,8 @@ import * as readline from 'node:readline';
8
8
  // read->merge->write, backup before the first byte, temp+fsync+rename commit)
9
9
  // rather than a bare readFile/writeFile pair.
10
10
  import { acquireLock, atomicReplace, backupConfig, realpathOrSelf, releaseLock, restoreFromBackup, } from '../commands/mcp-registration.js';
11
+ import { findExecutableOnPath } from './resolve-executable.js';
12
+ import { planResolvedCommandSpawn } from './windows-spawn.js';
11
13
  /**
12
14
  * The home directory every runtime's config is resolved under.
13
15
  *
@@ -23,6 +25,11 @@ import { acquireLock, atomicReplace, backupConfig, realpathOrSelf, releaseLock,
23
25
  export function defaultHomeDir() {
24
26
  return process.env.HOME?.trim() ? process.env.HOME : os.homedir();
25
27
  }
28
+ const RUNTIME_LABEL = {
29
+ codex: 'Codex',
30
+ grok: 'Grok',
31
+ claude: 'Claude Code',
32
+ };
26
33
  const CODEX_REQUEST_TIMEOUT_MS = 10_000;
27
34
  function errorMessage(value) {
28
35
  return value instanceof Error ? value.message : String(value);
@@ -77,11 +84,32 @@ function hqProjectHooks(hqRoot, hooks) {
77
84
  !hook.isManaged &&
78
85
  isWithin(path.resolve(hook.sourcePath), codexLayer));
79
86
  }
87
+ /**
88
+ * How to launch Codex. npm installs Codex on Windows as a `codex.cmd` shim,
89
+ * which `spawn('codex')` cannot find (ENOENT) and Node refuses to run without a
90
+ * shell, so a bare name is resolved through PATH + PATHEXT and a batch shim is
91
+ * routed through the escaped cmd.exe plan.
92
+ */
93
+ export function planCodexLaunch(executable, args, env = process.env, platform = process.platform, find = findExecutableOnPath) {
94
+ let command = executable;
95
+ if (platform === 'win32' && !path.win32.isAbsolute(executable) && !/[\\/]/.test(executable)) {
96
+ const resolved = find(executable, env.PATH ?? env.Path ?? '', platform, env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM');
97
+ if (!resolved) {
98
+ throw new Error(`Codex CLI not found on PATH (looked for ${executable}.exe and ${executable}.cmd); install Codex or add it to PATH, then rerun \`hq reindex\``);
99
+ }
100
+ command = resolved;
101
+ }
102
+ return planResolvedCommandSpawn(command, args, env, platform);
103
+ }
80
104
  /** Start a short-lived Codex app-server and complete its JSONL handshake. */
81
105
  export async function createCodexAppServerClient(cwd, executable = 'codex', args = ['app-server']) {
82
- const child = spawn(executable, args, {
106
+ const plan = planCodexLaunch(executable, args);
107
+ const child = spawn(plan.cmd, plan.args, {
83
108
  cwd,
84
109
  stdio: ['pipe', 'pipe', 'pipe'],
110
+ shell: plan.shell,
111
+ windowsHide: true,
112
+ ...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
85
113
  });
86
114
  const lines = readline.createInterface({ input: child.stdout });
87
115
  const pending = new Map();
@@ -692,7 +720,7 @@ export async function trustHqRuntimeHooks(hqRoot, deps = DEFAULT_DEPS, options =
692
720
  }
693
721
  }
694
722
  else if (result.status === 'failed') {
695
- console.warn(`reindex: could not trust ${result.runtime} HQ hooks: ${result.reason ?? 'unknown error'}`);
723
+ console.warn(`reindex: could not trust ${result.runtime} HQ hooks: ${result.reason ?? 'unknown error'}. ${RUNTIME_LABEL[result.runtime]} will not run HQ hooks until trust succeeds; fix the error above and rerun \`hq reindex\`.`);
696
724
  }
697
725
  }
698
726
  return results;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * npm-invocation: run npm without a shell on every platform.
3
+ *
4
+ * On Windows `npm` is a `npm.cmd` batch shim. `execFileSync('npm', ...)`
5
+ * cannot find it (ENOENT), and since Node's CVE-2024-27980 fix a `.cmd` file
6
+ * only runs through a shell, which would re-parse caller-supplied arguments
7
+ * such as a pack source. Instead, on Windows this resolves npm's JavaScript
8
+ * entry (`node_modules/npm/bin/npm-cli.js`, the file the shim itself runs)
9
+ * and launches it with the current Node binary as a plain argv array.
10
+ *
11
+ * Elsewhere `npm` is an executable on PATH and is used as is.
12
+ */
13
+ export interface NpmInvocation {
14
+ command: string;
15
+ args: string[];
16
+ }
17
+ export interface NpmInvocationEnv {
18
+ platform?: NodeJS.Platform;
19
+ execPath?: string;
20
+ env?: NodeJS.ProcessEnv;
21
+ exists?: (file: string) => boolean;
22
+ }
23
+ export declare class NpmNotFoundError extends Error {
24
+ constructor(searched: string[]);
25
+ }
26
+ export declare function npmInvocation(args: readonly string[], ctx?: NpmInvocationEnv): NpmInvocation;
27
+ //# sourceMappingURL=npm-invocation.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * npm-invocation: run npm without a shell on every platform.
3
+ *
4
+ * On Windows `npm` is a `npm.cmd` batch shim. `execFileSync('npm', ...)`
5
+ * cannot find it (ENOENT), and since Node's CVE-2024-27980 fix a `.cmd` file
6
+ * only runs through a shell, which would re-parse caller-supplied arguments
7
+ * such as a pack source. Instead, on Windows this resolves npm's JavaScript
8
+ * entry (`node_modules/npm/bin/npm-cli.js`, the file the shim itself runs)
9
+ * and launches it with the current Node binary as a plain argv array.
10
+ *
11
+ * Elsewhere `npm` is an executable on PATH and is used as is.
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ export class NpmNotFoundError extends Error {
16
+ constructor(searched) {
17
+ super('Could not find npm. Looked for npm-cli.js at: ' +
18
+ (searched.length > 0 ? searched.join(', ') : '(no candidate locations)') +
19
+ '. Install Node.js with npm, or make sure npm.cmd is on PATH, then retry.');
20
+ this.name = 'NpmNotFoundError';
21
+ }
22
+ }
23
+ export function npmInvocation(args, ctx = {}) {
24
+ const platform = ctx.platform ?? process.platform;
25
+ if (platform !== 'win32')
26
+ return { command: 'npm', args: [...args] };
27
+ const execPath = ctx.execPath ?? process.execPath;
28
+ const env = ctx.env ?? process.env;
29
+ const exists = ctx.exists ?? ((file) => fs.existsSync(file));
30
+ const win = path.win32;
31
+ const cliUnder = (dir) => win.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js');
32
+ // Prefer the npm the user's shell would run, so an npm installed or
33
+ // upgraded on its own (`npm i -g npm`, a version manager) wins over the
34
+ // copy bundled with node.exe.
35
+ const candidates = [];
36
+ // Set when hq itself runs under an npm script.
37
+ const npmExecPath = env.npm_execpath;
38
+ if (npmExecPath && win.basename(npmExecPath).toLowerCase() === 'npm-cli.js')
39
+ candidates.push(npmExecPath);
40
+ // The first npm.cmd shim on PATH, followed to the npm-cli.js beside it.
41
+ const pathValue = env.Path ?? env.PATH ?? '';
42
+ for (const dir of pathValue.split(';').filter(Boolean)) {
43
+ if (exists(win.join(dir, 'npm.cmd')))
44
+ candidates.push(cliUnder(dir));
45
+ }
46
+ // Fallback: the Node.js installer, nvm-windows and fnm put npm next to
47
+ // node.exe.
48
+ candidates.push(cliUnder(win.dirname(execPath)));
49
+ const cli = candidates.find((candidate) => exists(candidate));
50
+ if (!cli)
51
+ throw new NpmNotFoundError(candidates);
52
+ return { command: execPath, args: [cli, ...args] };
53
+ }
54
+ //# sourceMappingURL=npm-invocation.js.map
@@ -48,7 +48,9 @@ export const QMD_NATIVE_BINDING_REMEDY = "hq's local search index can't start: i
48
48
  "not built during install. pnpm 10 skips dependency build scripts by default, " +
49
49
  "which leaves better-sqlite3 uncompiled. Approve the build and reinstall — for " +
50
50
  "a global install run `pnpm approve-builds -g better-sqlite3` (drop -g for a " +
51
- "local project) — or reinstall hq with npm, then run the command again.";
51
+ "local project). npm 11.16 and later skip install scripts unless you opt in; reinstall with " +
52
+ "`npm install -g @indigoai-us/hq-cli --allow-scripts=better-sqlite3`. " +
53
+ "Or reinstall hq with npm, then run the command again.";
52
54
  /**
53
55
  * The text to inspect. For a QmdExitError the captured stderr/stdout is the qmd
54
56
  * process's real diagnostic; the synthesized `message` (which embeds the user's
@@ -1,2 +1,4 @@
1
+ /** First executable file on PATH for `command` (PATHEXT-expanded on Windows), or undefined. */
2
+ export declare function findExecutableOnPath(command: string, pathValue?: string, platform?: NodeJS.Platform, pathExt?: string): string | undefined;
1
3
  export declare function isExecutableOnPath(command: string, pathValue?: string, platform?: NodeJS.Platform, pathExt?: string): boolean;
2
4
  //# sourceMappingURL=resolve-executable.d.ts.map
@@ -9,8 +9,9 @@ function executableCandidates(command, pathValue = process.env.PATH ?? '', platf
9
9
  : pathValue.split(platform === 'win32' ? ';' : path.delimiter).filter(Boolean).flatMap((directory) => extensions.map((extension) => path.join(directory, `${command}${extension}`)));
10
10
  return candidates;
11
11
  }
12
- export function isExecutableOnPath(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
13
- return executableCandidates(command, pathValue, platform, pathExt).some((candidate) => {
12
+ /** First executable file on PATH for `command` (PATHEXT-expanded on Windows), or undefined. */
13
+ export function findExecutableOnPath(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
14
+ return executableCandidates(command, pathValue, platform, pathExt).find((candidate) => {
14
15
  try {
15
16
  fs.accessSync(candidate, fs.constants.X_OK);
16
17
  return fs.statSync(candidate).isFile();
@@ -20,4 +21,7 @@ export function isExecutableOnPath(command, pathValue = process.env.PATH ?? '',
20
21
  }
21
22
  });
22
23
  }
24
+ export function isExecutableOnPath(command, pathValue = process.env.PATH ?? '', platform = process.platform, pathExt = process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM') {
25
+ return findExecutableOnPath(command, pathValue, platform, pathExt) !== undefined;
26
+ }
23
27
  //# sourceMappingURL=resolve-executable.js.map
@@ -0,0 +1,50 @@
1
+ export type LinkKind = 'symlink' | 'junction' | 'copy';
2
+ export interface LinkDeps {
3
+ platform?: NodeJS.Platform;
4
+ /** Copy the folder when Windows refuses both link types. Default true. */
5
+ allowCopy?: boolean;
6
+ }
7
+ /** Marker file written into a skill folder that HQ copied instead of linked. */
8
+ export declare const SKILL_COPY_MARKER = ".hq-skill-copy";
9
+ /** Thrown when Windows refuses a directory symlink, a junction and a copy. */
10
+ export declare class LinkUnavailableError extends Error {
11
+ readonly linkPath: string;
12
+ readonly expected: true;
13
+ constructor(linkPath: string, cause: unknown);
14
+ }
15
+ /**
16
+ * Link `linkPath` to the directory `target`. On Windows a privilege refusal
17
+ * falls back to a junction (absolute target, no privilege needed), and a
18
+ * refused junction falls back to a marked copy unless `allowCopy` is false.
19
+ * Other platforms keep the plain symlink behaviour.
20
+ */
21
+ export declare function linkDirectory(target: string, linkPath: string, deps?: LinkDeps): LinkKind;
22
+ /**
23
+ * The source folder recorded in a skill copy's marker, or null when `p` is
24
+ * not a real folder carrying a valid marker.
25
+ */
26
+ export declare function skillCopySource(p: string): string | null;
27
+ /**
28
+ * Remove an HQ skill entry: a link (never recursing into it) or a folder HQ
29
+ * copied and marked. Refuses an unmarked real folder. A marked copy is kept,
30
+ * with a notice through `onKept`, when its marker names a different entry
31
+ * (the user renamed or duplicated it) or its files changed since HQ copied
32
+ * them. Returns false when nothing exists at `p` or the copy was kept.
33
+ */
34
+ export declare function removeSkillEntry(p: string, onKept?: (line: string) => void): boolean;
35
+ /**
36
+ * Remove whatever is at `p` without following any link: a link at `p` goes
37
+ * through `removeLinkOnly`, and a real folder is removed entry by entry so a
38
+ * link nested inside it is removed as a link. With `force`, a missing path is
39
+ * not an error.
40
+ */
41
+ export declare function removeEntryNoFollow(p: string, opts?: {
42
+ force?: boolean;
43
+ }): boolean;
44
+ /**
45
+ * Remove a symlink or junction without recursing into it. Refuses anything
46
+ * that is not a link, so a real folder is never deleted through this path.
47
+ * Returns false when nothing exists at `linkPath`.
48
+ */
49
+ export declare function removeLinkOnly(linkPath: string): boolean;
50
+ //# sourceMappingURL=safe-link.d.ts.map