@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
@@ -28,7 +28,7 @@ import http from "node:http";
28
28
  import https from "node:https";
29
29
  import path from "node:path";
30
30
  import { Readable } from "node:stream";
31
- import { formatCloudAssetBlock, formatCloudPlaceholder, parseCloudPlaceholder, } from "@indigoai-us/hq-cloud";
31
+ import { formatCloudAssetBlock, formatCloudPlaceholder, isCoveredByAny, parseCloudPlaceholder, resolvePullScope, } from "@indigoai-us/hq-cloud";
32
32
  import { PLACEHOLDER_SUFFIX, companyOffloadKey, containedPath, durablePlaceholderFs, renderPointer, resolvePlaceholder, } from "./storage-offload.js";
33
33
  import { formatPlanGateError, formatUpgradeRefusal, planGateErrorFromResponse, } from "../utils/plan-gate-error.js";
34
34
  /** Metadata key hq-pro binds to `checksumSha256` (sync's content hash). */
@@ -321,7 +321,74 @@ export async function runLibraryAdd(inputs, options, deps) {
321
321
  .catch((err) => ({ status: "failed", note: err.message }));
322
322
  }
323
323
  }
324
- return { company: slug, items };
324
+ const written = items.flatMap((item) => (item.ok && item.placeholderPath ? [item.placeholderPath] : []));
325
+ if (written.length === 0)
326
+ return { company: slug, items };
327
+ const syncScope = await ensureLibraryInSyncScope(company, dirRel, written, deps);
328
+ return { company: slug, items, ...(syncScope ? { syncScope } : {}) };
329
+ }
330
+ const LIBRARY_FOLDER = "library/";
331
+ /**
332
+ * A `shared` or `custom` sync pushes only paths inside the membership's
333
+ * prefix set (explicit grants plus pins), so a placeholder written outside it
334
+ * never reaches the vault. Pin it with `hq files get`'s pin writer: the
335
+ * default library folder as a whole, and for any other `--to` folder only the
336
+ * written placeholders, so the rest of that folder stays out of sync.
337
+ */
338
+ export async function ensureLibraryInSyncScope(company, dirRel, placeholderPaths, deps) {
339
+ if (!deps.syncScope || !deps.addPin)
340
+ return undefined;
341
+ const companyRel = (rel) => rel.split("/").slice(2).join("/");
342
+ const isLibrary = `${companyRel(dirRel)}/` === LIBRARY_FOLDER;
343
+ const targets = isLibrary ? [LIBRARY_FOLDER] : placeholderPaths.map(companyRel);
344
+ const command = isLibrary ? `hq files get ${LIBRARY_FOLDER} --company ${company.slug}` : undefined;
345
+ let scope;
346
+ try {
347
+ scope = await deps.syncScope(company);
348
+ }
349
+ catch (err) {
350
+ return {
351
+ status: "not-pinned",
352
+ pins: targets,
353
+ ...(command ? { command } : {}),
354
+ reason: `could not read your sync scope: ${err.message}`,
355
+ };
356
+ }
357
+ const missing = scope.syncMode === "all" ? [] : targets.filter((t) => !isCoveredByAny(t, scope.prefixSet ?? []));
358
+ if (missing.length === 0)
359
+ return { status: "in-scope", pins: [] };
360
+ try {
361
+ for (const target of missing)
362
+ deps.addPin(deps.hqRoot, company.slug, target);
363
+ }
364
+ catch (err) {
365
+ return {
366
+ status: "not-pinned",
367
+ pins: missing,
368
+ ...(command ? { command } : {}),
369
+ reason: `could not write the pin: ${err.message}`,
370
+ };
371
+ }
372
+ return { status: "pinned", pins: missing };
373
+ }
374
+ /**
375
+ * {@link resolvePullScope} answers `all` when any call fails. Report the
376
+ * failure instead, so a failed lookup is not mistaken for full sync.
377
+ */
378
+ export async function resolveLibrarySyncScope(client, company, hqRoot) {
379
+ let failure;
380
+ const watch = (call) => call.catch((err) => {
381
+ failure ??= err;
382
+ throw err;
383
+ });
384
+ const scope = await resolvePullScope({
385
+ listMyMemberships: () => watch(client.listMyMemberships()),
386
+ getMembershipSyncConfig: (id) => watch(client.getMembershipSyncConfig(id)),
387
+ listMyExplicitGrants: (uid) => watch(client.listMyExplicitGrants(uid)),
388
+ }, company.uid, company.slug, hqRoot);
389
+ if (failure !== undefined)
390
+ throw failure instanceof Error ? failure : new Error(String(failure));
391
+ return scope;
325
392
  }
326
393
  async function writeAssetPlaceholder(p, options, deps, dirAbs) {
327
394
  const ts = offloadTimestamp(deps.now());
@@ -35,7 +35,7 @@ import { findHqRoot } from "../utils/manifest.js";
35
35
  import { networkTransportErrorCode } from "../utils/network-transport-error.js";
36
36
  import { resolveVaultCredentialForCapability } from "../utils/resolve-vault-credential.js";
37
37
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
38
- import { defaultLibraryIo, runLibraryAdd, runLibraryTag, } from "./library-add.js";
38
+ import { defaultLibraryIo, resolveLibrarySyncScope, runLibraryAdd, runLibraryTag, } from "./library-add.js";
39
39
  import { captionAddedAsset, extractCaptionFrames, runLibraryDescribe, runLocalClaude } from "./library-caption.js";
40
40
  import { LIBRARY_LINK_DEFAULT_MINUTES, LIBRARY_LINK_MAX_MINUTES, runLibraryLink } from "./library-link.js";
41
41
  /** Mirrors hq-pro's MAX_ASSET_SEARCH_TAGS / tag length bound. */
@@ -295,8 +295,39 @@ export function formatLibraryAddResult(result) {
295
295
  return `${line}\n ${chalk.dim(`AI caption added (${item.caption.captionedBy}).`)}`;
296
296
  return `${line}\n ${chalk.yellow(item.caption.note ?? "Captioning skipped.")}`;
297
297
  })
298
+ .concat(formatLibrarySyncScope(result))
298
299
  .join("\n");
299
300
  }
301
+ function formatLibrarySyncScope({ company, syncScope }) {
302
+ if (!syncScope || syncScope.status === "in-scope")
303
+ return [];
304
+ const pins = syncScope.pins.map((pin) => `${company}:${pin}`).join(", ");
305
+ if (syncScope.status === "pinned") {
306
+ return [chalk.dim(`Pinned ${pins} so sync uploads these placeholders to the company vault.`)];
307
+ }
308
+ const fix = syncScope.command
309
+ ? `Pin the folder with: ${syncScope.command}`
310
+ : `Add ${syncScope.pins.join(", ")} under "${company}" in .hq/pins.json at the HQ root.`;
311
+ return [
312
+ chalk.yellow(`Warning: sync may not upload these placeholders (${syncScope.reason}). ` +
313
+ `Until it does, teammates and cloud search will not see them. ${fix}`),
314
+ ];
315
+ }
316
+ /** Sync scope wiring for `hq library add`; an API key has no sync tree to check. */
317
+ async function librarySyncScopeDeps(credential, hqRoot) {
318
+ if (credential.kind !== "cognito")
319
+ return {};
320
+ const [{ VaultClient }, { buildVaultConfig }, { addPin }] = await Promise.all([
321
+ import("@indigoai-us/hq-cloud"),
322
+ import("../utils/cognito-session.js"),
323
+ import("./files-browse.js"),
324
+ ]);
325
+ const client = new VaultClient(buildVaultConfig(credential.token));
326
+ return {
327
+ syncScope: (company) => resolveLibrarySyncScope(client, company, hqRoot),
328
+ addPin,
329
+ };
330
+ }
300
331
  /** GET a presigned URL. Errors never echo the URL, which carries a signature. */
301
332
  async function downloadPresigned(url) {
302
333
  const res = await fetch(url);
@@ -366,9 +397,11 @@ export function registerLibraryCommand(program) {
366
397
  .option("--json", "Print machine-readable JSON")
367
398
  .action(async (sources, options) => {
368
399
  try {
369
- const { token } = await resolveVaultCredentialForCapability("files:write", "library add", {
400
+ const credential = await resolveVaultCredentialForCapability("files:write", "library add", {
370
401
  interactive: true,
371
402
  });
403
+ const { token } = credential;
404
+ const hqRoot = findHqRoot();
372
405
  const result = await runLibraryAdd(sources, {
373
406
  company: options.company,
374
407
  to: options.to,
@@ -376,11 +409,12 @@ export function registerLibraryCommand(program) {
376
409
  title: options.title,
377
410
  note: options.note,
378
411
  }, {
379
- hqRoot: findHqRoot(),
412
+ hqRoot,
380
413
  now: () => new Date(),
381
414
  resolveCompany: (slug) => libraryCompany(token, slug),
382
415
  presign: (body) => vaultApiFetch({ token, path: "/v1/files/presign", method: "POST", body }),
383
416
  ...defaultLibraryIo,
417
+ ...(await librarySyncScopeDeps(credential, hqRoot)),
384
418
  ...(options.caption === false
385
419
  ? {}
386
420
  : {
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Flow:
24
24
  * 1. Classify source → transport
25
- * 2. Fetch payload to tmpdir (npm pack | git clone | rsync)
25
+ * 2. Fetch payload to tmpdir (npm pack | git clone | local copy)
26
26
  * 3. Parse + validate package.yaml (10 checks from spec)
27
27
  * 4. Evaluate `conditional` predicate — skip if exits non-zero
28
28
  * 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
@@ -438,7 +438,7 @@ export interface InstallPackOptions {
438
438
  * Route this function's human output to stderr (and silence scan-packages
439
439
  * stdout) so a caller emitting machine-readable JSON keeps stdout clean.
440
440
  * The fetch tools are already quiet/stderr-only (`npm pack --silent`,
441
- * `rsync -a`, `git clone` progress -> stderr), so this is sufficient.
441
+ * `git clone` progress -> stderr), so this is sufficient.
442
442
  */
443
443
  quiet?: boolean;
444
444
  /**
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Flow:
24
24
  * 1. Classify source → transport
25
- * 2. Fetch payload to tmpdir (npm pack | git clone | rsync)
25
+ * 2. Fetch payload to tmpdir (npm pack | git clone | local copy)
26
26
  * 3. Parse + validate package.yaml (10 checks from spec)
27
27
  * 4. Evaluate `conditional` predicate — skip if exits non-zero
28
28
  * 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
@@ -38,7 +38,7 @@ import * as os from 'os';
38
38
  import * as path from 'path';
39
39
  import * as readline from 'readline';
40
40
  import { parseUserYaml } from '../utils/user-yaml-error.js';
41
- import { createHash, createPublicKey, verify as cryptoVerify, } from 'node:crypto';
41
+ import { createHash, createPublicKey, randomBytes, verify as cryptoVerify, } from 'node:crypto';
42
42
  import { trustedMarketplaceSigningKey } from '../lib/pack-signing-keys.js';
43
43
  import { execFileSync, spawnSync } from 'child_process';
44
44
  import chalk from 'chalk';
@@ -51,6 +51,8 @@ import { codexTargets } from '../lib/install/codex-targets.js';
51
51
  import { readHqVersion, routeContribution, listInstalledPacks, mcpServerNameForContribution } from '../utils/pack-contributions.js';
52
52
  import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
53
53
  import { safeExtractTarball } from './safe-extract.js';
54
+ import { copyTree, removeTreeNoFollow } from '../utils/copy-tree.js';
55
+ import { npmInvocation } from '../utils/npm-invocation.js';
54
56
  import { runResidentCommandWithPackageLease } from '../utils/resident-package-preload.js';
55
57
  import { getCompanyUid, vaultApiFetch, vaultApiFetchPublic, } from '../utils/vault-api.js';
56
58
  import { ensureCognitoToken } from '../utils/cognito-session.js';
@@ -116,6 +118,18 @@ async function latestWithDiskCache(cacheKey, opts, refresh) {
116
118
  export const MARKETPLACE_PREFIX = 'marketplace:';
117
119
  /** Pinned source allowed to claim the reserved hq-anywhere pack name via its CLI alias. */
118
120
  export const PINNED_HQ_ANYWHERE_SOURCE = 'github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere';
121
+ /**
122
+ * Windows path forms a user (or `os.tmpdir()`) hands to `hq install`: a drive
123
+ * path (`C:\packs\x`, `C:/packs/x`), a UNC path (`\\server\share\x`), or a
124
+ * backslash-relative path (`.\x`, `..\x`). None of these can be a registry
125
+ * slug, npm name or git URL, so they always mean the local transport.
126
+ */
127
+ function isWindowsPathSource(source) {
128
+ return (/^[A-Za-z]:[\\/]/.test(source) ||
129
+ source.startsWith('\\\\') ||
130
+ source.startsWith('.\\') ||
131
+ source.startsWith('..\\'));
132
+ }
119
133
  export function classify(source) {
120
134
  // US-006: marketplace:<slug>[@version] is recognized BEFORE the legacy
121
135
  // registry fallback and ahead of every other transport. The slug is resolved
@@ -144,7 +158,8 @@ export function classify(source) {
144
158
  if (source.startsWith('./') ||
145
159
  source.startsWith('../') ||
146
160
  source.startsWith('/') ||
147
- source.startsWith('file:')) {
161
+ source.startsWith('file:') ||
162
+ isWindowsPathSource(source)) {
148
163
  return 'local';
149
164
  }
150
165
  // Fallback: bare slug — caller (dispatcher) routes those to legacy flow.
@@ -204,8 +219,10 @@ function fetchNpm(source, tmpDir, integrity) {
204
219
  // Use `npm pack` to grab the tarball without actually installing anything.
205
220
  // Capture output to get the produced filename. Arguments are passed as an
206
221
  // argv array — never interpolated into a shell string — so `source` cannot
207
- // break out even if it contains metacharacters.
208
- const out = execFileSync('npm', ['pack', '--silent', '--pack-destination', tmpDir, source], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'inherit'] });
222
+ // break out even if it contains metacharacters. On Windows npm is a .cmd
223
+ // shim, so npmInvocation runs npm-cli.js with node instead of a shell.
224
+ const npm = npmInvocation(['pack', '--silent', '--pack-destination', tmpDir, source]);
225
+ const out = execFileSync(npm.command, npm.args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'inherit'] });
209
226
  const tarball = out.trim().split('\n').filter(Boolean).pop();
210
227
  if (!tarball) {
211
228
  throw new Error(`npm pack produced no tarball for "${source}"`);
@@ -557,7 +574,7 @@ function fetchGit(source, tmpDir, followBranch) {
557
574
  }
558
575
  payloadDir = path.join(tmpDir, 'payload');
559
576
  fs.mkdirSync(payloadDir, { recursive: true });
560
- rsyncDir(subAbs, payloadDir);
577
+ copyPayloadDir(subAbs, payloadDir);
561
578
  }
562
579
  else {
563
580
  // Drop .git — the pack should be file content, not a nested repo
@@ -581,24 +598,17 @@ function fetchLocal(source, tmpDir) {
581
598
  }
582
599
  const payloadDir = path.join(tmpDir, 'local');
583
600
  fs.mkdirSync(payloadDir, { recursive: true });
584
- rsyncDir(abs, payloadDir);
601
+ copyPayloadDir(abs, payloadDir);
585
602
  return { payloadDir, resolvedSource: clean };
586
603
  }
587
604
  /**
588
- * Invoke rsync with an argv array (no shell). Trailing slashes on the source
589
- * path are preserved by passing the exact strings through execFileSync.
605
+ * Copy a pack source directory's contents into the payload dir, skipping VCS
606
+ * metadata, installed dependencies and Finder files at any depth. This is the
607
+ * Node equivalent of the earlier `rsync -a --exclude=...` call, so it also
608
+ * works on Windows, which ships no rsync. Links are copied as links.
590
609
  */
591
- function rsyncDir(src, dest) {
592
- const srcSlashed = src.endsWith('/') ? src : `${src}/`;
593
- const destSlashed = dest.endsWith('/') ? dest : `${dest}/`;
594
- execFileSync('rsync', [
595
- '-a',
596
- '--exclude=.git',
597
- '--exclude=node_modules',
598
- '--exclude=.DS_Store',
599
- srcSlashed,
600
- destSlashed,
601
- ], { stdio: 'inherit' });
610
+ function copyPayloadDir(src, dest) {
611
+ copyTree(src, dest, { exclude: ['.git', 'node_modules', '.DS_Store'] });
602
612
  }
603
613
  /**
604
614
  * Ask git whether <ref> resolves as a named branch or tag on <url>. Returns
@@ -1640,33 +1650,121 @@ export function installToPackages(payloadDir, pkg, hqRoot) {
1640
1650
  const packagesDir = path.join(hqRoot, 'core', 'packages');
1641
1651
  fs.mkdirSync(packagesDir, { recursive: true });
1642
1652
  const destDir = path.join(packagesDir, pkg.name);
1643
- // SECURITY/ATOMICITY (US-020): wire atomically. rsync into a sibling staging
1653
+ // SECURITY/ATOMICITY (US-020): wire atomically. Copy into a sibling staging
1644
1654
  // dir on the SAME filesystem first; only swap it into the final destination
1645
- // once the copy fully succeeds. A failed/partial rsync therefore never
1655
+ // once the copy fully succeeds. A failed/partial copy therefore never
1646
1656
  // leaves a half-wired pack dir in place (which scan-packages.sh would then
1647
1657
  // wire into host paths). On any error the staging dir is rolled back.
1648
1658
  const stagingDir = fs.mkdtempSync(path.join(packagesDir, `.${pkg.name}.staging-`));
1659
+ let asideDir;
1660
+ // Set when the staging dir holds the only complete copy of the new pack and
1661
+ // must survive the cleanup below.
1662
+ let keepStaging = false;
1649
1663
  try {
1650
- // rsync preserves modes/symlinks; argv form — no shell.
1651
- const srcSlashed = payloadDir.endsWith('/') ? payloadDir : `${payloadDir}/`;
1652
- const stagingSlashed = stagingDir.endsWith('/') ? stagingDir : `${stagingDir}/`;
1653
- execFileSync('rsync', ['-a', srcSlashed, stagingSlashed], { stdio: 'inherit' });
1654
- // Swap into place: remove any prior install, then atomic rename. The brief
1655
- // window between rm and rename is unavoidable with a same-name dir, but the
1656
- // staged copy guarantees the NEW contents are fully present before we touch
1657
- // the live location.
1658
- if (fs.existsSync(destDir)) {
1659
- fs.rmSync(destDir, { recursive: true, force: true });
1664
+ // Keeps modes, mtimes and symlinks (as links) like `rsync -a`, without
1665
+ // spawning rsync, which Windows does not ship. `finalRoot` makes Windows
1666
+ // junction fallbacks point into destDir, where the copy ends up after the
1667
+ // rename below, so they do not dangle once stagingDir is gone.
1668
+ copyTree(payloadDir, stagingDir, { finalRoot: destDir });
1669
+ // Swap into place. The prior install is renamed aside (not deleted) first,
1670
+ // so if the second rename fails it can be put back and the old pack stays
1671
+ // installed. The aside name is unique and not pre-created: on Windows a
1672
+ // rename onto an existing directory fails.
1673
+ if (lstatOrNull(destDir)) {
1674
+ const aside = path.join(packagesDir, `.${pkg.name}.old-${process.pid}-${Date.now()}-${randomBytes(4).toString('hex')}`);
1675
+ fs.renameSync(destDir, aside);
1676
+ asideDir = aside;
1677
+ }
1678
+ try {
1679
+ fs.renameSync(stagingDir, destDir);
1680
+ }
1681
+ catch (err) {
1682
+ if (asideDir) {
1683
+ try {
1684
+ fs.renameSync(asideDir, destDir);
1685
+ asideDir = undefined;
1686
+ }
1687
+ catch (restoreErr) {
1688
+ // Nothing is installed at destDir now. Keep both copies so either
1689
+ // can be moved into place by hand.
1690
+ keepStaging = true;
1691
+ throw new Error(`Could not move the new ${pkg.name} into ${destDir} (${errorMessage(err)}), and could not restore ` +
1692
+ `the previous install from ${asideDir} (${errorMessage(restoreErr)}). Nothing is installed at ` +
1693
+ `${destDir}. The previous install is at ${asideDir} and the complete new copy is at ${stagingDir}. ` +
1694
+ `Move one of them to ${destDir} by hand.`, { cause: err });
1695
+ }
1696
+ }
1697
+ throw err;
1660
1698
  }
1661
- fs.renameSync(stagingDir, destDir);
1662
1699
  }
1663
1700
  finally {
1664
- if (fs.existsSync(stagingDir)) {
1665
- fs.rmSync(stagingDir, { recursive: true, force: true });
1701
+ // Cleanup must never replace the error that got us here, so failures are
1702
+ // reported and left behind rather than thrown.
1703
+ if (!keepStaging) {
1704
+ try {
1705
+ removeTreeNoFollow(stagingDir);
1706
+ }
1707
+ catch (cleanupErr) {
1708
+ process.stderr.write(`warning: could not remove staging dir ${stagingDir}: ${errorMessage(cleanupErr)}\n`);
1709
+ }
1710
+ }
1711
+ }
1712
+ // The new pack is in place. Links inside the old install are removed as
1713
+ // links, never followed, so data they point at survives a reinstall.
1714
+ if (asideDir) {
1715
+ try {
1716
+ removeTreeNoFollow(asideDir);
1717
+ }
1718
+ catch (cleanupErr) {
1719
+ process.stderr.write(`warning: installed ${pkg.name}, but could not remove the previous install at ${asideDir}: ` +
1720
+ `${errorMessage(cleanupErr)}. Delete it by hand.\n`);
1666
1721
  }
1667
1722
  }
1723
+ sweepLeftovers(packagesDir, pkg.name);
1668
1724
  return destDir;
1669
1725
  }
1726
+ /**
1727
+ * Remove `.<name>.staging-*` and `.<name>.old-*` siblings that an earlier
1728
+ * interrupted or failed install of the same pack left behind. Runs only after
1729
+ * a new install is in place, so a copy kept for manual recovery is not removed
1730
+ * while nothing else is installed. Links inside are removed as links, and a
1731
+ * failure only warns.
1732
+ */
1733
+ function sweepLeftovers(packagesDir, name) {
1734
+ const prefixes = [`.${name}.staging-`, `.${name}.old-`];
1735
+ let entries;
1736
+ try {
1737
+ entries = fs.readdirSync(packagesDir);
1738
+ }
1739
+ catch (err) {
1740
+ process.stderr.write(`warning: could not list ${packagesDir} for leftover install folders: ${errorMessage(err)}\n`);
1741
+ return;
1742
+ }
1743
+ for (const entry of entries) {
1744
+ if (!prefixes.some((prefix) => entry.startsWith(prefix)))
1745
+ continue;
1746
+ const leftover = path.join(packagesDir, entry);
1747
+ try {
1748
+ removeTreeNoFollow(leftover);
1749
+ }
1750
+ catch (err) {
1751
+ process.stderr.write(`warning: could not remove leftover install folder ${leftover}: ${errorMessage(err)}\n`);
1752
+ }
1753
+ }
1754
+ }
1755
+ function lstatOrNull(target) {
1756
+ try {
1757
+ return fs.lstatSync(target);
1758
+ }
1759
+ catch (err) {
1760
+ if (err.code === 'ENOENT')
1761
+ return null;
1762
+ throw err;
1763
+ }
1764
+ }
1765
+ function errorMessage(err) {
1766
+ return err instanceof Error ? err.message : String(err);
1767
+ }
1670
1768
  /**
1671
1769
  * Stamp the install-time `source:` into the destination package.yaml. This is
1672
1770
  * the only on-disk record of which `recommended_packages[].source` produced
@@ -1,3 +1,5 @@
1
+ import { reconcileHookRegistrations } from '../lib/hook-engine/reconcile.js';
2
+ import { trustCodexUserHooks } from '../utils/hook-trust.js';
1
3
  /**
2
4
  * hq reindex — surface namespaced skills and commands, clean up retired
3
5
  * personal-overlay mirror links, capture coding-harness session logs, regenerate
@@ -775,6 +777,23 @@ export function registerReindexCommand(program) {
775
777
  // means a sync/rescue holds the shared operation lock and may be
776
778
  // rewriting .claude/settings.json right now, and this is an unlocked
777
779
  // whole-file read-modify-write.
780
+ // Registration reconcile is best-effort: a failure leaves the current
781
+ // registrations in place and must not stop the rest of reindex.
782
+ try {
783
+ // A file that failed is left as it was; the keys of files that were
784
+ // rewritten are still trusted so Codex keeps running their hooks.
785
+ const { codexUserKeys, failures } = reconcileHookRegistrations(hqRoot);
786
+ for (const failure of failures)
787
+ console.warn(`reindex: hook registration reconcile skipped ${failure}`);
788
+ if (codexUserKeys.length) {
789
+ const trust = await trustCodexUserHooks(hqRoot, codexUserKeys);
790
+ if (trust.status === 'failed')
791
+ console.warn(`reindex: Codex hook trust needs repair: ${trust.reason}`);
792
+ }
793
+ }
794
+ catch (error) {
795
+ console.warn(`reindex: hook registration reconcile skipped: ${error.message}`);
796
+ }
778
797
  dropProjectDefaultModeForBypassUser(hqRoot);
779
798
  await trustHqRuntimeHooks(hqRoot, undefined, {
780
799
  rootAliases: [literalHqRoot],
@@ -42,17 +42,10 @@
42
42
  */
43
43
  import { Command } from "commander";
44
44
  import type { ManifestUploadTransport, RunManifestUploadPassOptions, RunManifestUploadPassResult } from "@indigoai-us/hq-cloud";
45
+ import { MANIFEST_DISABLED_ENV, manifestUploadsDisabled } from "../lib/manifest-disabled.js";
45
46
  /** The server route the pass POSTs each chunk to. */
46
47
  export declare const MANIFEST_UPLOAD_PATH = "/v1/sync-manifest/upload";
47
- /**
48
- * The kill switch hq-cloud's pass honours. Restated locally (rather than
49
- * imported) for two reasons: it lives in the same unreleased module as the
50
- * manifest exports, and — more importantly — we must be able to read it
51
- * BEFORE deciding to do any work at all.
52
- */
53
- export declare const MANIFEST_DISABLED_ENV = "HQ_SYNC_MANIFEST_DISABLED";
54
- /** True when the kill switch is set to anything other than empty/0/false. */
55
- export declare function manifestUploadsDisabled(env?: NodeJS.ProcessEnv): boolean;
48
+ export { MANIFEST_DISABLED_ENV, manifestUploadsDisabled };
56
49
  /** The literal `--scope` value that selects the personal (non-company) tree. */
57
50
  export declare const PERSONAL_SCOPE = "personal";
58
51
  /**
@@ -51,6 +51,7 @@ import chalk from "chalk";
51
51
  // at runtime and so cost nothing here.
52
52
  import { VaultClient } from "@indigoai-us/hq-cloud";
53
53
  import { MANIFEST_MIN_HQ_CLOUD_VERSION, manifestExportsAvailable, requireManifestExports, } from "../lib/hq-cloud-manifest.js";
54
+ import { MANIFEST_DISABLED_ENV, manifestUploadsDisabled, } from "../lib/manifest-disabled.js";
54
55
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
55
56
  import { loadClientHealthState, persistClientHealthState, } from "../utils/client-health.js";
56
57
  import { vaultApiFetch } from "../utils/vault-api.js";
@@ -58,21 +59,9 @@ import { readActiveCompanySlug } from "./sync-mode.js";
58
59
  // ── Constants ───────────────────────────────────────────────────────────────
59
60
  /** The server route the pass POSTs each chunk to. */
60
61
  export const MANIFEST_UPLOAD_PATH = "/v1/sync-manifest/upload";
61
- /**
62
- * The kill switch hq-cloud's pass honours. Restated locally (rather than
63
- * imported) for two reasons: it lives in the same unreleased module as the
64
- * manifest exports, and — more importantly — we must be able to read it
65
- * BEFORE deciding to do any work at all.
66
- */
67
- export const MANIFEST_DISABLED_ENV = "HQ_SYNC_MANIFEST_DISABLED";
68
- /** True when the kill switch is set to anything other than empty/0/false. */
69
- export function manifestUploadsDisabled(env = process.env) {
70
- const raw = env[MANIFEST_DISABLED_ENV];
71
- if (raw === undefined)
72
- return false;
73
- const value = raw.trim().toLowerCase();
74
- return value !== "" && value !== "0" && value !== "false";
75
- }
62
+ // The kill switch lives in a leaf module so `hq doctor` can read it without
63
+ // loading this command. Re-exported for existing importers.
64
+ export { MANIFEST_DISABLED_ENV, manifestUploadsDisabled };
76
65
  /** The literal `--scope` value that selects the personal (non-company) tree. */
77
66
  export const PERSONAL_SCOPE = "personal";
78
67
  // ── Pure helpers ────────────────────────────────────────────────────────────
@@ -0,0 +1,4 @@
1
+ /** Dependency-free routing: this command must never load the online CLI graph. */
2
+ export declare function hookEnginePathArgs(argv: readonly string[]): string[] | null;
3
+ export declare function runHookEnginePath(args: readonly string[]): Promise<void>;
4
+ //# sourceMappingURL=hook-engine-path.d.ts.map
@@ -0,0 +1,43 @@
1
+ /** Dependency-free routing: this command must never load the online CLI graph. */
2
+ export function hookEnginePathArgs(argv) {
3
+ if (argv[2] !== 'core')
4
+ return null;
5
+ let i = 3;
6
+ if (argv[i] === '--hq-root' && argv[i + 1])
7
+ i += 2;
8
+ else if (argv[i]?.startsWith('--hq-root='))
9
+ i++;
10
+ return argv[i] === 'hook-engine-path' ? argv.slice(i + 1) : null;
11
+ }
12
+ export async function runHookEnginePath(args) {
13
+ try {
14
+ if (args.some(arg => !['--repair', '--help', '-h', '--for-registration'].includes(arg))) {
15
+ throw new Error('Usage: hq core hook-engine-path [--repair] [--for-registration]');
16
+ }
17
+ if (args.includes('--help') || args.includes('-h')) {
18
+ console.log('Usage: hq core hook-engine-path [--repair] [--for-registration]\nResolve and pin the installed native hook engine offline.\n--for-registration prints a path only when the launcher flag is enabled.');
19
+ return;
20
+ }
21
+ const { installHookEngine, hookEngineRequired, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
22
+ if (args.includes('--for-registration')) {
23
+ const { launcherEnabled } = await import('./lib/hook-engine/registration.js');
24
+ if (!launcherEnabled()) {
25
+ process.exitCode = 1;
26
+ return;
27
+ }
28
+ }
29
+ const binary = installHookEngine({ repair: args.includes('--repair') });
30
+ if (binary)
31
+ console.log(binary);
32
+ else {
33
+ console.error(`Native hook engine missing; ${hookEngineRequired() ? 'repair before running hooks' : 'existing bash hooks remain active'}. Repair: ${HOOK_ENGINE_REPAIR}`);
34
+ process.exitCode = 1;
35
+ }
36
+ }
37
+ catch (error) {
38
+ // Catch locally: the global rejection reporter imports main.js and Sentry.
39
+ console.error(error instanceof Error ? error.message : String(error));
40
+ process.exitCode = 1;
41
+ }
42
+ }
43
+ //# sourceMappingURL=hook-engine-path.js.map