@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.
- package/CHANGELOG.md +18 -0
- package/dist/command-catalog.generated.d.ts +24 -0
- package/dist/command-catalog.generated.js +26 -0
- package/dist/commands/bot.js +7 -2
- package/dist/commands/capacity.js +10 -2
- package/dist/commands/cloud.d.ts +14 -2
- package/dist/commands/cloud.js +25 -16
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +225 -4
- package/dist/commands/integrations-core.d.ts +3 -0
- package/dist/commands/integrations-core.js +11 -0
- package/dist/commands/integrations-manage.js +2 -2
- package/dist/commands/integrations.js +5 -5
- package/dist/commands/sync-core.js +4 -1
- package/dist/lib/bot/claude-login.d.ts +7 -1
- package/dist/lib/bot/claude-login.js +56 -16
- package/dist/lib/bot/executive-settings.d.ts +1 -1
- package/dist/lib/bot/executive-settings.js +8 -6
- package/dist/lib/bot/jobs/meeting-detect.js +2 -2
- package/dist/lib/bot/jobs/meeting-rider.js +2 -2
- package/dist/lib/bot/run.js +3 -3
- package/dist/lib/capacity/codex-availability.d.ts +6 -0
- package/dist/lib/capacity/codex-availability.js +33 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +53 -5
- package/dist/lib/capacity/collectors/claude-oauth.js +251 -71
- package/dist/lib/capacity/ledger.d.ts +0 -11
- package/dist/lib/capacity/ledger.js +30 -3
- package/dist/lib/capacity/route.js +11 -5
- package/dist/lib/daemon/catalog.d.ts +3 -0
- package/dist/lib/daemon/catalog.js +17 -4
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +9 -2
- package/dist/lib/daemon/install.d.ts +99 -0
- package/dist/lib/daemon/install.js +316 -7
- package/dist/lib/daemon/migrate.d.ts +3 -1
- package/dist/lib/daemon/migrate.js +14 -2
- package/dist/lib/daemon/plist-refresh.d.ts +34 -0
- package/dist/lib/daemon/plist-refresh.js +62 -0
- package/dist/lib/daemon/run.js +27 -2
- package/dist/lib/daemon/selfcheck.d.ts +71 -0
- package/dist/lib/daemon/selfcheck.js +150 -0
- package/dist/lib/daemon/supervisor.d.ts +31 -0
- package/dist/lib/daemon/supervisor.js +130 -6
- package/dist/lib/daemon/unit-holds.js +2 -1
- package/dist/lib/lanes/config.js +13 -0
- package/dist/lib/lanes/throttle.d.ts +12 -1
- package/dist/lib/lanes/throttle.js +27 -8
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/service-manager/launchd.d.ts +25 -0
- package/dist/lib/service-manager/launchd.js +51 -3
- package/dist/lib/service-manager/migration.d.ts +14 -0
- package/dist/lib/service-manager/migration.js +34 -0
- package/dist/lib/service-manager/systemd.d.ts +13 -0
- package/dist/lib/service-manager/systemd.js +19 -0
- package/dist/lib/service-manager/types.d.ts +8 -0
- package/dist/lib/signals/steps/extract.d.ts +110 -4
- package/dist/lib/signals/steps/extract.js +520 -39
- package/dist/lib/sync-core/engine-home.d.ts +2 -1
- package/dist/lib/sync-core/engine-home.js +25 -8
- package/dist/lib/sync-core/reconcile.d.ts +7 -2
- package/dist/lib/sync-core/reconcile.js +32 -9
- package/dist/lib/sync-core/release.d.ts +35 -3
- package/dist/lib/sync-core/release.js +95 -12
- 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
154
|
-
const prepared = path.join(staging,
|
|
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.
|
|
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.
|
|
121
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
123
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
124
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
125
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
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
|
}
|