@indigoai-us/hq-cli 5.345.202 → 5.345.204

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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/command-catalog.generated.d.ts +24 -0
  3. package/dist/command-catalog.generated.js +26 -0
  4. package/dist/commands/bot.js +7 -2
  5. package/dist/commands/capacity.js +10 -2
  6. package/dist/commands/cloud.d.ts +14 -2
  7. package/dist/commands/cloud.js +25 -16
  8. package/dist/commands/daemon.d.ts +2 -1
  9. package/dist/commands/daemon.js +225 -4
  10. package/dist/commands/integrations-core.d.ts +3 -0
  11. package/dist/commands/integrations-core.js +11 -0
  12. package/dist/commands/integrations-manage.js +2 -2
  13. package/dist/commands/integrations.js +5 -5
  14. package/dist/commands/sync-core.js +4 -1
  15. package/dist/lib/bot/claude-login.d.ts +7 -1
  16. package/dist/lib/bot/claude-login.js +56 -16
  17. package/dist/lib/bot/executive-settings.d.ts +1 -1
  18. package/dist/lib/bot/executive-settings.js +8 -6
  19. package/dist/lib/bot/jobs/meeting-detect.js +2 -2
  20. package/dist/lib/bot/jobs/meeting-rider.js +2 -2
  21. package/dist/lib/bot/run.js +3 -3
  22. package/dist/lib/capacity/codex-availability.d.ts +6 -0
  23. package/dist/lib/capacity/codex-availability.js +33 -0
  24. package/dist/lib/capacity/collectors/claude-oauth.d.ts +53 -5
  25. package/dist/lib/capacity/collectors/claude-oauth.js +251 -71
  26. package/dist/lib/capacity/ledger.d.ts +0 -11
  27. package/dist/lib/capacity/ledger.js +30 -3
  28. package/dist/lib/capacity/route.js +11 -5
  29. package/dist/lib/daemon/catalog.d.ts +3 -0
  30. package/dist/lib/daemon/catalog.js +17 -4
  31. package/dist/lib/daemon/handoff.d.ts +2 -0
  32. package/dist/lib/daemon/handoff.js +9 -2
  33. package/dist/lib/daemon/install.d.ts +99 -0
  34. package/dist/lib/daemon/install.js +316 -7
  35. package/dist/lib/daemon/migrate.d.ts +3 -1
  36. package/dist/lib/daemon/migrate.js +14 -2
  37. package/dist/lib/daemon/plist-refresh.d.ts +34 -0
  38. package/dist/lib/daemon/plist-refresh.js +62 -0
  39. package/dist/lib/daemon/run.js +27 -2
  40. package/dist/lib/daemon/selfcheck.d.ts +71 -0
  41. package/dist/lib/daemon/selfcheck.js +150 -0
  42. package/dist/lib/daemon/supervisor.d.ts +31 -0
  43. package/dist/lib/daemon/supervisor.js +130 -6
  44. package/dist/lib/daemon/unit-holds.js +2 -1
  45. package/dist/lib/lanes/config.js +13 -0
  46. package/dist/lib/lanes/throttle.d.ts +12 -1
  47. package/dist/lib/lanes/throttle.js +27 -8
  48. package/dist/lib/lanes/types.d.ts +5 -0
  49. package/dist/lib/service-manager/launchd.d.ts +25 -0
  50. package/dist/lib/service-manager/launchd.js +51 -3
  51. package/dist/lib/service-manager/migration.d.ts +14 -0
  52. package/dist/lib/service-manager/migration.js +34 -0
  53. package/dist/lib/service-manager/systemd.d.ts +13 -0
  54. package/dist/lib/service-manager/systemd.js +19 -0
  55. package/dist/lib/service-manager/types.d.ts +8 -0
  56. package/dist/lib/signals/steps/extract.d.ts +110 -4
  57. package/dist/lib/signals/steps/extract.js +520 -39
  58. package/dist/lib/sync-core/engine-home.d.ts +2 -1
  59. package/dist/lib/sync-core/engine-home.js +25 -8
  60. package/dist/lib/sync-core/reconcile.d.ts +7 -2
  61. package/dist/lib/sync-core/reconcile.js +32 -9
  62. package/dist/lib/sync-core/release.d.ts +35 -3
  63. package/dist/lib/sync-core/release.js +95 -12
  64. package/package.json +7 -7
@@ -17,13 +17,14 @@ export declare const SYNC_CORE_REQUESTS_PER_DAY = 20000;
17
17
  export declare function syncCoreEngineHome(home: string, vaultUid: string): string;
18
18
  export declare function syncCoreUnitId(vaultUid: string): string;
19
19
  export declare function isSyncCoreVaultUid(value: unknown): value is string;
20
- export declare function engineConfigToml(vault: SyncCoreVault, hqRoot: string, phase: SyncCoreVaultPhase): string;
20
+ export declare function engineConfigToml(vault: SyncCoreVault, hqRoot: string, phase: SyncCoreVaultPhase, tokenPath?: string): string;
21
21
  export interface WriteEngineHomeOptions {
22
22
  home: string;
23
23
  hqRoot: string;
24
24
  vault: SyncCoreVault;
25
25
  phase: SyncCoreVaultPhase;
26
26
  tokenFile: string;
27
+ platform?: NodeJS.Platform;
27
28
  }
28
29
  export declare function writeEngineHome(opts: WriteEngineHomeOptions): {
29
30
  engineHome: string;
@@ -11,7 +11,7 @@ export function syncCoreUnitId(vaultUid) {
11
11
  export function isSyncCoreVaultUid(value) {
12
12
  return typeof value === "string" && /^(prs|cmp)_[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value);
13
13
  }
14
- export function engineConfigToml(vault, hqRoot, phase) {
14
+ export function engineConfigToml(vault, hqRoot, phase, tokenPath) {
15
15
  const lines = [
16
16
  "[sync]",
17
17
  'engine = "rust-test"',
@@ -20,6 +20,7 @@ export function engineConfigToml(vault, hqRoot, phase) {
20
20
  `vaults = [${JSON.stringify(vault.uid)}]`,
21
21
  `hq_root = ${JSON.stringify(hqRoot)}`,
22
22
  `requests_per_day = ${SYNC_CORE_REQUESTS_PER_DAY}`,
23
+ ...(tokenPath ? [`token_path = ${JSON.stringify(tokenPath)}`] : []),
23
24
  ];
24
25
  if (vault.kind === "company") {
25
26
  lines.push("[sync.roots]", `${vault.uid} = ${JSON.stringify(`companies/${vault.slug}`)}`);
@@ -52,15 +53,24 @@ function writeIfChanged(file, text) {
52
53
  fs.renameSync(tmp, file);
53
54
  return true;
54
55
  }
56
+ /**
57
+ * The files an engine home links to on macOS and Linux. Windows needs a privilege to create a
58
+ * symlink, so there the config names the token file and the engine reads `menubar.json` and
59
+ * `work-mesh/` from `HQ_STATE_DIR`, which the daemon sets.
60
+ */
61
+ const ENGINE_HOME_LINKS = ["menubar.json", "work-mesh", "cognito-tokens.json"];
55
62
  export function writeEngineHome(opts) {
56
63
  const engineHome = syncCoreEngineHome(opts.home, opts.vault.uid);
57
64
  const hq = path.join(engineHome, ".hq");
58
65
  const root = path.join(hq, "sync-core");
59
66
  fs.mkdirSync(path.join(root, "owned"), { recursive: true, mode: 0o700 });
60
- replaceSymlink(path.join(hq, "menubar.json"), path.join(opts.home, ".hq", "menubar.json"));
61
- replaceSymlink(path.join(hq, "work-mesh"), path.join(opts.home, ".hq", "work-mesh"));
62
- replaceSymlink(path.join(hq, "cognito-tokens.json"), opts.tokenFile);
63
- const configChanged = writeIfChanged(path.join(root, "config.toml"), engineConfigToml(opts.vault, opts.hqRoot, opts.phase));
67
+ const windows = (opts.platform ?? process.platform) === "win32";
68
+ if (!windows) {
69
+ replaceSymlink(path.join(hq, "menubar.json"), path.join(opts.home, ".hq", "menubar.json"));
70
+ replaceSymlink(path.join(hq, "work-mesh"), path.join(opts.home, ".hq", "work-mesh"));
71
+ replaceSymlink(path.join(hq, "cognito-tokens.json"), opts.tokenFile);
72
+ }
73
+ const configChanged = writeIfChanged(path.join(root, "config.toml"), engineConfigToml(opts.vault, opts.hqRoot, opts.phase, windows ? opts.tokenFile : undefined));
64
74
  const ownedPath = path.join(root, "owned", `${opts.vault.uid}.json`);
65
75
  let ownedChanged = false;
66
76
  if (opts.phase === "full") {
@@ -123,8 +133,9 @@ function blocked(plan, blockedReason) {
123
133
  return { ...plan, blockedReason };
124
134
  }
125
135
  export function inspectEngineHome(home, hqRoot, plan, platform) {
126
- if (platform !== "darwin" && platform !== "linux")
127
- return blocked(plan, "the sync engine requires macOS or Linux");
136
+ if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
137
+ return blocked(plan, "the sync engine requires macOS, Linux or Windows");
138
+ }
128
139
  try {
129
140
  if (!fs.statSync(plan.binaryPath).isFile())
130
141
  return blocked(plan, "the sync engine binary is missing");
@@ -182,7 +193,13 @@ export function inspectEngineHome(home, hqRoot, plan, platform) {
182
193
  else if (fs.existsSync(ownedPath)) {
183
194
  return blocked(plan, `the engine ownership record for ${plan.uid} must not exist while the index warms`);
184
195
  }
185
- for (const link of ["menubar.json", "work-mesh", "cognito-tokens.json"]) {
196
+ if (platform === "win32") {
197
+ if (typeof sync.token_path !== "string" || !path.isAbsolute(sync.token_path)) {
198
+ return blocked(plan, `the engine config for ${plan.uid} must name the token file`);
199
+ }
200
+ return plan;
201
+ }
202
+ for (const link of ENGINE_HOME_LINKS) {
186
203
  try {
187
204
  fs.readlinkSync(path.join(plan.engineHome, ".hq", link));
188
205
  }
@@ -1,5 +1,5 @@
1
1
  import { type SyncCoreVault } from "./engine-home.js";
2
- import { type SyncCoreBinaryInstall } from "./release.js";
2
+ import { type SyncCoreBinaryInstall, type SyncCorePin } from "./release.js";
3
3
  import { type SyncCoreRolloutState } from "./rollout-state.js";
4
4
  export interface SyncCoreMembership {
5
5
  companyUid: string;
@@ -14,7 +14,12 @@ export interface SyncCoreReconcileDeps {
14
14
  resolvePersonUid: () => Promise<string | null>;
15
15
  readRolloutFlag: (companyUid: string) => Promise<boolean>;
16
16
  ensureBinary: (release: string) => Promise<SyncCoreBinaryInstall>;
17
+ /** A fixed release (tests). Otherwise readPin decides, then SYNC_CORE_RELEASE. */
17
18
  release?: string;
19
+ platform?: NodeJS.Platform;
20
+ env?: NodeJS.ProcessEnv;
21
+ /** Reads the server-side engine pin; never throws (see readSyncCorePin). */
22
+ readPin?: () => Promise<SyncCorePin>;
18
23
  now?: () => Date;
19
24
  log?: (line: string) => void;
20
25
  }
@@ -26,6 +31,6 @@ export interface SyncCoreReconcileResult {
26
31
  kept: string[];
27
32
  warnings: string[];
28
33
  }
29
- export declare function fullModeHold(vault: SyncCoreVault, release: string): string | undefined;
34
+ export declare function fullModeHold(vault: SyncCoreVault, release: string, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string | undefined;
30
35
  export declare function reconcileSyncCore(deps: SyncCoreReconcileDeps): Promise<SyncCoreReconcileResult>;
31
36
  //# sourceMappingURL=reconcile.d.ts.map
@@ -3,16 +3,21 @@ import * as path from "node:path";
3
3
  import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
4
4
  import { readVaultSyncSwitches, writeVaultSyncSwitches } from "../vault-sync-switches.js";
5
5
  import { clearEngineStatus, engineFirstCycle, isSyncCoreVaultUid, removeEngineOwnership, syncCoreEngineHome, writeEngineHome, } from "./engine-home.js";
6
- import { SYNC_CORE_FULL_RELEASE, SYNC_CORE_RELEASE, fullModeAllowed, installedSyncCoreRelease, syncCoreBinaryPath, } from "./release.js";
6
+ import { SYNC_CORE_FULL_RELEASE, SYNC_CORE_RELEASE, compareSyncCoreReleases, fullModeAllowed, installedSyncCoreRelease, syncCoreBinaryPath, windowsFullRelease, } from "./release.js";
7
7
  import { readSyncCoreSkips } from "./skip.js";
8
8
  import { SYNC_RUST_ENGINE_ROLLOUT_FLAG } from "./flag.js";
9
9
  import { readSyncCoreRolloutState, syncCoreRolloutStatePath, writeSyncCoreRolloutState, } from "./rollout-state.js";
10
10
  function message(error) {
11
11
  return error instanceof Error ? error.message : String(error);
12
12
  }
13
- export function fullModeHold(vault, release) {
14
- if (fullModeAllowed(release))
13
+ export function fullModeHold(vault, release, platform = process.platform, env = process.env) {
14
+ if (fullModeAllowed(release, platform, env))
15
15
  return undefined;
16
+ if (platform === "win32" && fullModeAllowed(release, "linux", env)) {
17
+ const windows = windowsFullRelease(env);
18
+ const needed = windows ? `engine release ${windows} or later` : "an engine release cleared for full sync on Windows";
19
+ return `${vault.kind} vaults on Windows move to the new sync engine with ${needed} (${release} available); staying on the current sync`;
20
+ }
16
21
  const needed = SYNC_CORE_FULL_RELEASE ? `engine release ${SYNC_CORE_FULL_RELEASE} or later` : "an engine release cleared for full sync";
17
22
  return `${vault.kind} vaults move to the new sync engine with ${needed} (${release} installed); staying on the current sync`;
18
23
  }
@@ -65,8 +70,10 @@ function ownedVaultsWithoutState(home, hqRoot) {
65
70
  export async function reconcileSyncCore(deps) {
66
71
  const now = deps.now ?? (() => new Date());
67
72
  const log = deps.log ?? (() => { });
68
- const release = deps.release ?? SYNC_CORE_RELEASE;
73
+ const platform = deps.platform ?? process.platform;
74
+ const env = deps.env ?? process.env;
69
75
  const warnings = [];
76
+ let release = deps.release ?? SYNC_CORE_RELEASE;
70
77
  const previous = readSyncCoreRolloutState(deps.home);
71
78
  const previousVaults = previous?.vaults ?? ownedVaultsWithoutState(deps.home, deps.hqRoot);
72
79
  const previousByUid = new Map(previousVaults.map((vault) => [vault.uid, vault]));
@@ -114,9 +121,26 @@ export async function reconcileSyncCore(deps) {
114
121
  catch (error) {
115
122
  warnings.push(message(error));
116
123
  }
117
- let binaryPath = syncCoreBinaryPath(deps.home);
118
- let installedRelease = installedSyncCoreRelease(deps.home);
124
+ let binaryPath = syncCoreBinaryPath(deps.home, platform);
125
+ let installedRelease = installedSyncCoreRelease(deps.home, platform);
126
+ const held = [];
127
+ const best = installedRelease && compareSyncCoreReleases(installedRelease, release) >= 0 ? installedRelease : release;
128
+ if (platform === "win32" && !fullModeAllowed(best, platform, env)) {
129
+ for (const vault of desired)
130
+ held.push({ uid: vault.uid, reason: fullModeHold(vault, best, platform, env) });
131
+ desired.length = 0;
132
+ }
119
133
  if (desired.length > 0) {
134
+ if (deps.release === undefined && deps.readPin) {
135
+ const pin = await deps.readPin();
136
+ release = pin.release;
137
+ if (pin.warning)
138
+ warnings.push(pin.warning);
139
+ if (pin.source === "server" && installedRelease && compareSyncCoreReleases(pin.release, installedRelease) < 0) {
140
+ // The no-downgrade guard in ensureSyncCoreBinary keeps the newer engine.
141
+ log(`the server pins hq-syncd ${pin.release}, older than the installed ${installedRelease}; keeping ${installedRelease}`);
142
+ }
143
+ }
120
144
  try {
121
145
  const install = await deps.ensureBinary(release);
122
146
  binaryPath = install.binaryPath;
@@ -130,10 +154,9 @@ export async function reconcileSyncCore(deps) {
130
154
  desired.length = 0;
131
155
  }
132
156
  }
133
- const held = [];
134
157
  const engineRelease = installedRelease ?? release;
135
158
  for (let i = desired.length - 1; i >= 0; i -= 1) {
136
- const reason = fullModeHold(desired[i], engineRelease);
159
+ const reason = fullModeHold(desired[i], engineRelease, platform, env);
137
160
  if (!reason)
138
161
  continue;
139
162
  held.unshift({ uid: desired[i].uid, reason });
@@ -152,7 +175,7 @@ export async function reconcileSyncCore(deps) {
152
175
  const engineHome = syncCoreEngineHome(deps.home, vault.uid);
153
176
  if (!before)
154
177
  clearEngineStatus(deps.home, vault.uid);
155
- writeEngineHome({ home: deps.home, hqRoot: deps.hqRoot, vault, phase: "full", tokenFile: deps.tokenFile });
178
+ writeEngineHome({ home: deps.home, hqRoot: deps.hqRoot, vault, phase: "full", tokenFile: deps.tokenFile, platform });
156
179
  const wasFull = before?.phase === "full";
157
180
  const alreadyOff = vault.kind === "personal"
158
181
  ? switchesBefore.personalOff
@@ -1,16 +1,48 @@
1
1
  export declare const SYNC_CORE_RELEASE = "v0.1.23";
2
2
  export declare const SYNC_CORE_FULL_RELEASE: string | null;
3
+ /**
4
+ * The first engine release whose Windows build runs full sync. Until it is set, Windows vaults
5
+ * stay on the current sync and no Windows engine is downloaded. `HQ_SYNC_CORE_WINDOWS_FULL_RELEASE`
6
+ * overrides it on one machine, for testing a build before it is released.
7
+ */
8
+ export declare const SYNC_CORE_WINDOWS_FULL_RELEASE: string | null;
9
+ export declare const SYNC_CORE_WINDOWS_FULL_RELEASE_ENV = "HQ_SYNC_CORE_WINDOWS_FULL_RELEASE";
3
10
  export declare function compareSyncCoreReleases(a: string, b: string): number;
4
- export declare function fullModeAllowed(release: string): boolean;
11
+ export declare function windowsFullRelease(env?: NodeJS.ProcessEnv): string | null;
12
+ export declare function fullModeAllowed(release: string, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
5
13
  export declare const SYNC_CORE_DOWNLOAD_BASE = "https://indigoai-public.s3.amazonaws.com/hq-sync-core";
6
14
  export declare const SYNC_CORE_DOWNLOAD_BASE_ENV = "HQ_SYNC_CORE_DOWNLOAD_BASE";
7
- export declare function syncCoreBinaryPath(home?: string): string;
15
+ /** The server-side engine pin, published next to the release folders on the download base. */
16
+ export declare const SYNC_CORE_PIN_FILE = "pin.json";
17
+ export interface SyncCorePin {
18
+ /** The engine release machines should run. */
19
+ release: string;
20
+ /** "server": read from pin.json; "built-in": SYNC_CORE_RELEASE, because no server pin could be used. */
21
+ source: "server" | "built-in";
22
+ /** Why the server pin was not used, when it exists but could not be read. A missing pin.json sets none. */
23
+ warning?: string;
24
+ }
25
+ /**
26
+ * The engine release to install, read from `<download base>/pin.json`
27
+ * (`{"release":"v0.1.24"}`), so an engine release reaches machines without an
28
+ * hq-cli release. Falls back to SYNC_CORE_RELEASE when the file is missing,
29
+ * unreadable or malformed. Never throws. The pin only picks a version: on
30
+ * Windows the release gate (fullModeAllowed) still decides whether it installs.
31
+ */
32
+ export declare function readSyncCorePin(opts?: {
33
+ env?: NodeJS.ProcessEnv;
34
+ baseUrl?: string;
35
+ fetch?: typeof fetch;
36
+ timeoutMs?: number;
37
+ }): Promise<SyncCorePin>;
38
+ export declare function syncCoreBinaryName(platform?: NodeJS.Platform): string;
39
+ export declare function syncCoreBinaryPath(home?: string, platform?: NodeJS.Platform): string;
8
40
  export declare function syncCoreReleaseMarkerPath(home?: string): string;
9
41
  export declare function syncCoreAssetName(release: string, platform: NodeJS.Platform, arch: string): string | null;
10
42
  export declare function syncCoreDownloadBase(env?: NodeJS.ProcessEnv): string;
11
43
  export declare function parseSha256Sums(text: string): Map<string, string>;
12
44
  export declare function syncCoreBinaryVersion(binaryPath: string): string | null;
13
- export declare function installedSyncCoreRelease(home: string): string | null;
45
+ export declare function installedSyncCoreRelease(home: string, platform?: NodeJS.Platform): string | null;
14
46
  export interface EnsureSyncCoreBinaryOptions {
15
47
  home: string;
16
48
  platform: NodeJS.Platform;
@@ -5,6 +5,13 @@ import * as path from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  export const SYNC_CORE_RELEASE = "v0.1.23";
7
7
  export const SYNC_CORE_FULL_RELEASE = "v0.1.23";
8
+ /**
9
+ * The first engine release whose Windows build runs full sync. Until it is set, Windows vaults
10
+ * stay on the current sync and no Windows engine is downloaded. `HQ_SYNC_CORE_WINDOWS_FULL_RELEASE`
11
+ * overrides it on one machine, for testing a build before it is released.
12
+ */
13
+ export const SYNC_CORE_WINDOWS_FULL_RELEASE = null;
14
+ export const SYNC_CORE_WINDOWS_FULL_RELEASE_ENV = "HQ_SYNC_CORE_WINDOWS_FULL_RELEASE";
8
15
  export function compareSyncCoreReleases(a, b) {
9
16
  const parse = (release) => release.replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10) || 0);
10
17
  const [x, y] = [parse(a), parse(b)];
@@ -15,14 +22,65 @@ export function compareSyncCoreReleases(a, b) {
15
22
  }
16
23
  return 0;
17
24
  }
18
- export function fullModeAllowed(release) {
19
- return SYNC_CORE_FULL_RELEASE !== null && compareSyncCoreReleases(release, SYNC_CORE_FULL_RELEASE) >= 0;
25
+ export function windowsFullRelease(env = process.env) {
26
+ const override = env[SYNC_CORE_WINDOWS_FULL_RELEASE_ENV]?.trim();
27
+ return override && /^v?\d+\.\d+\.\d+$/.test(override) ? override : SYNC_CORE_WINDOWS_FULL_RELEASE;
28
+ }
29
+ export function fullModeAllowed(release, platform = process.platform, env = process.env) {
30
+ if (SYNC_CORE_FULL_RELEASE === null || compareSyncCoreReleases(release, SYNC_CORE_FULL_RELEASE) < 0)
31
+ return false;
32
+ if (platform !== "win32")
33
+ return true;
34
+ const windows = windowsFullRelease(env);
35
+ return windows !== null && compareSyncCoreReleases(release, windows) >= 0;
20
36
  }
21
37
  export const SYNC_CORE_DOWNLOAD_BASE = "https://indigoai-public.s3.amazonaws.com/hq-sync-core";
22
38
  export const SYNC_CORE_DOWNLOAD_BASE_ENV = "HQ_SYNC_CORE_DOWNLOAD_BASE";
23
39
  const DOWNLOAD_TIMEOUT_MS = 600_000;
24
- export function syncCoreBinaryPath(home = os.homedir()) {
25
- return path.join(home, ".hq", "bin", "hq-syncd");
40
+ /** The server-side engine pin, published next to the release folders on the download base. */
41
+ export const SYNC_CORE_PIN_FILE = "pin.json";
42
+ const PIN_TIMEOUT_MS = 10_000;
43
+ const RELEASE_PATTERN = /^v\d+\.\d+\.\d+$/;
44
+ /**
45
+ * The engine release to install, read from `<download base>/pin.json`
46
+ * (`{"release":"v0.1.24"}`), so an engine release reaches machines without an
47
+ * hq-cli release. Falls back to SYNC_CORE_RELEASE when the file is missing,
48
+ * unreadable or malformed. Never throws. The pin only picks a version: on
49
+ * Windows the release gate (fullModeAllowed) still decides whether it installs.
50
+ */
51
+ export async function readSyncCorePin(opts = {}) {
52
+ const builtIn = (warning) => ({ release: SYNC_CORE_RELEASE, source: "built-in", ...(warning ? { warning } : {}) });
53
+ const url = `${opts.baseUrl?.replace(/\/+$/, "") ?? syncCoreDownloadBase(opts.env)}/${SYNC_CORE_PIN_FILE}`;
54
+ let response;
55
+ try {
56
+ response = await (opts.fetch ?? fetch)(url, { signal: AbortSignal.timeout(opts.timeoutMs ?? PIN_TIMEOUT_MS) });
57
+ }
58
+ catch (error) {
59
+ return builtIn(`could not read the engine pin ${url}: ${error instanceof Error ? error.message : String(error)}; using built-in ${SYNC_CORE_RELEASE}`);
60
+ }
61
+ // S3 answers 403 for a missing key on a bucket without public listing.
62
+ if (response.status === 404 || response.status === 403)
63
+ return builtIn();
64
+ if (!response.ok)
65
+ return builtIn(`could not read the engine pin ${url}: HTTP ${response.status}; using built-in ${SYNC_CORE_RELEASE}`);
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(await response.text());
69
+ }
70
+ catch {
71
+ return builtIn(`the engine pin ${url} is not JSON; using built-in ${SYNC_CORE_RELEASE}`);
72
+ }
73
+ const release = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.release : undefined;
74
+ if (typeof release !== "string" || !RELEASE_PATTERN.test(release)) {
75
+ return builtIn(`the engine pin ${url} has no valid "release" (want vX.Y.Z); using built-in ${SYNC_CORE_RELEASE}`);
76
+ }
77
+ return { release, source: "server" };
78
+ }
79
+ export function syncCoreBinaryName(platform = process.platform) {
80
+ return platform === "win32" ? "hq-syncd.exe" : "hq-syncd";
81
+ }
82
+ export function syncCoreBinaryPath(home = os.homedir(), platform = process.platform) {
83
+ return path.join(home, ".hq", "bin", syncCoreBinaryName(platform));
26
84
  }
27
85
  export function syncCoreReleaseMarkerPath(home = os.homedir()) {
28
86
  return path.join(home, ".hq", "bin", "hq-syncd.release");
@@ -34,6 +92,10 @@ export function syncCoreAssetName(release, platform, arch) {
34
92
  return `hq-sync-core-${release}-linux-x64.tar.gz`;
35
93
  if (platform === "linux" && arch === "arm64")
36
94
  return `hq-sync-core-${release}-linux-arm64.tar.gz`;
95
+ if (platform === "win32" && arch === "x64")
96
+ return `hq-sync-core-${release}-windows-x64.zip`;
97
+ if (platform === "win32" && arch === "arm64")
98
+ return `hq-sync-core-${release}-windows-arm64.zip`;
37
99
  return null;
38
100
  }
39
101
  export function syncCoreDownloadBase(env = process.env) {
@@ -59,9 +121,9 @@ export function syncCoreBinaryVersion(binaryPath) {
59
121
  return null;
60
122
  }
61
123
  }
62
- export function installedSyncCoreRelease(home) {
124
+ export function installedSyncCoreRelease(home, platform = process.platform) {
63
125
  try {
64
- const binary = syncCoreBinaryPath(home);
126
+ const binary = syncCoreBinaryPath(home, platform);
65
127
  if (!fs.statSync(binary).isFile())
66
128
  return null;
67
129
  fs.accessSync(binary, fs.constants.X_OK);
@@ -91,6 +153,24 @@ function findFile(dir, name) {
91
153
  }
92
154
  return null;
93
155
  }
156
+ /**
157
+ * Windows cannot replace an executable that is running, but it can rename one. The old engine
158
+ * keeps running from `<binary>.old` until its unit restarts; the copy left by the previous
159
+ * update is removed first when nothing runs from it any more.
160
+ */
161
+ function moveRunningBinaryAside(binaryPath) {
162
+ if (!fs.existsSync(binaryPath))
163
+ return;
164
+ const aside = `${binaryPath}.old`;
165
+ try {
166
+ fs.rmSync(aside, { force: true });
167
+ }
168
+ catch {
169
+ // still running from the previous update; that copy is removed on a later one
170
+ }
171
+ const target = fs.existsSync(aside) ? `${binaryPath}.${process.pid}.old` : aside;
172
+ fs.renameSync(binaryPath, target);
173
+ }
94
174
  async function download(fetcher, url) {
95
175
  let response;
96
176
  try {
@@ -105,8 +185,8 @@ async function download(fetcher, url) {
105
185
  }
106
186
  export async function ensureSyncCoreBinary(opts) {
107
187
  const release = opts.release ?? SYNC_CORE_RELEASE;
108
- const binaryPath = syncCoreBinaryPath(opts.home);
109
- const installedRelease = installedSyncCoreRelease(opts.home);
188
+ const binaryPath = syncCoreBinaryPath(opts.home, opts.platform);
189
+ const installedRelease = installedSyncCoreRelease(opts.home, opts.platform);
110
190
  if (installedRelease && compareSyncCoreReleases(installedRelease, release) >= 0 && fs.existsSync(binaryPath)) {
111
191
  const marker = syncCoreReleaseMarkerPath(opts.home);
112
192
  let current = "";
@@ -147,13 +227,16 @@ export async function ensureSyncCoreBinary(opts) {
147
227
  fs.writeFileSync(archive, bytes, { mode: 0o600 });
148
228
  const extracted = path.join(staging, "extracted");
149
229
  fs.mkdirSync(extracted, { mode: 0o700 });
150
- run("tar", ["-xzf", archive, "-C", extracted]);
151
- const unpacked = findFile(extracted, "hq-syncd");
230
+ run("tar", [asset.endsWith(".zip") ? "-xf" : "-xzf", archive, "-C", extracted]);
231
+ const name = syncCoreBinaryName(opts.platform);
232
+ const unpacked = findFile(extracted, name);
152
233
  if (!unpacked)
153
- throw new SyncCoreReleaseError(`${asset} does not contain hq-syncd`);
154
- const prepared = path.join(staging, "hq-syncd");
234
+ throw new SyncCoreReleaseError(`${asset} does not contain ${name}`);
235
+ const prepared = path.join(staging, name);
155
236
  fs.copyFileSync(unpacked, prepared);
156
237
  fs.chmodSync(prepared, 0o700);
238
+ if (opts.platform === "win32")
239
+ moveRunningBinaryAside(binaryPath);
157
240
  fs.renameSync(prepared, binaryPath);
158
241
  const marker = syncCoreReleaseMarkerPath(opts.home);
159
242
  fs.writeFileSync(`${marker}.tmp`, `${release}\n`, { mode: 0o600 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.202",
3
+ "version": "5.345.204",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -117,11 +117,11 @@
117
117
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
118
118
  },
119
119
  "optionalDependencies": {
120
- "@indigoai-us/hq-hook-win32-x64": "5.345.202",
121
- "@indigoai-us/hq-hook-win32-arm64": "5.345.202",
122
- "@indigoai-us/hq-hook-darwin-x64": "5.345.202",
123
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.202",
124
- "@indigoai-us/hq-hook-linux-x64": "5.345.202",
125
- "@indigoai-us/hq-hook-linux-arm64": "5.345.202"
120
+ "@indigoai-us/hq-hook-win32-x64": "5.345.204",
121
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.204",
122
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.204",
123
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.204",
124
+ "@indigoai-us/hq-hook-linux-x64": "5.345.204",
125
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.204"
126
126
  }
127
127
  }