@indigoai-us/hq-cli 5.345.175 → 5.345.177

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 (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/command-catalog.generated.d.ts +65 -0
  3. package/dist/command-catalog.generated.js +80 -0
  4. package/dist/commands/bot.d.ts +28 -2
  5. package/dist/commands/bot.js +29 -6
  6. package/dist/commands/daemon.js +16 -0
  7. package/dist/commands/lanes-loop.d.ts +4 -1
  8. package/dist/commands/lanes-loop.js +23 -4
  9. package/dist/commands/lanes-shared.d.ts +5 -0
  10. package/dist/commands/lanes.d.ts +5 -7
  11. package/dist/commands/lanes.js +65 -5
  12. package/dist/commands/sync-core.d.ts +17 -0
  13. package/dist/commands/sync-core.js +46 -5
  14. package/dist/commands/sync-manifest.d.ts +6 -0
  15. package/dist/commands/sync-manifest.js +13 -0
  16. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +448 -226
  17. package/dist/hq-cloud-sync-runner/package.json +1 -1
  18. package/dist/lib/bot/cli-update.d.ts +36 -0
  19. package/dist/lib/bot/cli-update.js +160 -42
  20. package/dist/lib/bot/run.d.ts +21 -0
  21. package/dist/lib/bot/run.js +127 -10
  22. package/dist/lib/bot/runtime/claude.d.ts +7 -1
  23. package/dist/lib/bot/runtime/claude.js +29 -0
  24. package/dist/lib/bot/runtime/codex.d.ts +5 -0
  25. package/dist/lib/bot/runtime/codex.js +13 -0
  26. package/dist/lib/bot/runtime/types.d.ts +10 -0
  27. package/dist/lib/bot/runtime-sign-in-check.d.ts +32 -0
  28. package/dist/lib/bot/runtime-sign-in-check.js +53 -0
  29. package/dist/lib/bot/runtime-sign-in.d.ts +32 -3
  30. package/dist/lib/bot/runtime-sign-in.js +17 -2
  31. package/dist/lib/bot/status.d.ts +25 -1
  32. package/dist/lib/bot/status.js +28 -0
  33. package/dist/lib/daemon/catalog.d.ts +4 -0
  34. package/dist/lib/daemon/catalog.js +47 -19
  35. package/dist/lib/daemon/run.js +1 -0
  36. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  37. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  38. package/dist/lib/lanes/loop-disposition.d.ts +44 -0
  39. package/dist/lib/lanes/loop-disposition.js +302 -0
  40. package/dist/lib/lanes/loop.d.ts +16 -3
  41. package/dist/lib/lanes/loop.js +167 -60
  42. package/dist/lib/lanes/reconcile.d.ts +5 -0
  43. package/dist/lib/lanes/reconcile.js +79 -4
  44. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +3 -0
  45. package/dist/lib/mesh/live/daemon/work-subscribe.js +41 -12
  46. package/dist/lib/sync-core/engine-home.d.ts +11 -4
  47. package/dist/lib/sync-core/engine-home.js +22 -21
  48. package/dist/lib/sync-core/manifest-scopes.d.ts +15 -0
  49. package/dist/lib/sync-core/manifest-scopes.js +26 -0
  50. package/dist/lib/sync-core/reconcile.d.ts +2 -2
  51. package/dist/lib/sync-core/reconcile.js +70 -46
  52. package/dist/lib/sync-core/release.d.ts +2 -2
  53. package/dist/lib/sync-core/release.js +31 -6
  54. package/dist/lib/sync-core/rollout-state.d.ts +6 -3
  55. package/dist/lib/sync-core/rollout-state.js +26 -22
  56. package/package.json +8 -8
@@ -72,10 +72,11 @@ export function writeEngineHome(opts) {
72
72
  }
73
73
  return { engineHome, changed: configChanged || ownedChanged };
74
74
  }
75
+ export const SYNC_CORE_STATUS_WORK_LIMIT = 20;
75
76
  function count(value) {
76
77
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
77
78
  }
78
- export function engineReadOnlyPlan(engineHome, vaultUid) {
79
+ export function engineFirstCycle(engineHome, vaultUid, since) {
79
80
  let status;
80
81
  try {
81
82
  status = record(JSON.parse(fs.readFileSync(path.join(engineHome, ".hq", "sync-core", "status.json"), "utf8")));
@@ -83,30 +84,30 @@ export function engineReadOnlyPlan(engineHome, vaultUid) {
83
84
  catch {
84
85
  return null;
85
86
  }
86
- if (record(status?.mode)?.value !== "readonly")
87
+ if (record(status?.mode)?.value !== "full")
87
88
  return null;
88
- const lastPlan = record(status?.last_plan);
89
- if (!lastPlan || typeof lastPlan.at_ms !== "number" || lastPlan.at_ms <= 0)
89
+ const cycle = record(status?.last_canary);
90
+ if (!cycle || cycle.vault !== vaultUid || cycle.full_vault !== true)
90
91
  return null;
91
- const vaults = Array.isArray(lastPlan.vaults) ? lastPlan.vaults : [];
92
- const run = vaults.map(record).find((v) => v?.vault === vaultUid);
93
- if (!run)
92
+ const sinceMs = Date.parse(since);
93
+ if (typeof cycle.at_ms !== "number" || cycle.at_ms <= 0 || !Number.isFinite(sinceMs) || cycle.at_ms < sinceMs)
94
94
  return null;
95
- const errors = Array.isArray(lastPlan.errors) ? lastPlan.errors : [];
96
- if (errors.some((e) => {
97
- const vault = record(e)?.vault;
98
- return vault === vaultUid || typeof vault !== "string";
99
- }))
100
- return null;
101
- const ops = record(record(run.plan)?.ops_by_action) ?? {};
95
+ const work = (Array.isArray(cycle.work) ? cycle.work : []).map(record).filter((item) => item !== undefined);
96
+ const actions = (...names) => work.filter((item) => names.includes(String(item.action))).length;
102
97
  return {
103
- at: new Date(lastPlan.at_ms).toISOString(),
104
- uploads: count(ops.upload),
105
- downloads: count(ops.download),
106
- deletes: count(ops.delete_local) + count(ops.delete_remote),
107
- conflicts: count(ops.conflict) + count(ops.merge),
108
- remoteRows: count(record(run.remote)?.rows),
109
- localFiles: count(record(run.local)?.files),
98
+ at: new Date(cycle.at_ms).toISOString(),
99
+ planned: count(cycle.planned),
100
+ completed: count(cycle.completed),
101
+ pending: count(cycle.pending),
102
+ held: count(cycle.held),
103
+ notApplied: count(cycle.not_applied),
104
+ uploads: actions("upload"),
105
+ downloads: actions("download"),
106
+ deletes: actions("delete_local", "delete_remote"),
107
+ conflicts: actions("conflict", "merge"),
108
+ deferred: actions("deferred"),
109
+ listed: work.length,
110
+ truncated: work.length >= SYNC_CORE_STATUS_WORK_LIMIT,
110
111
  };
111
112
  }
112
113
  export function clearEngineStatus(home, vaultUid) {
@@ -0,0 +1,15 @@
1
+ import type { VaultSyncSwitches } from "../vault-sync-switches.js";
2
+ export interface EngineManifestVault {
3
+ kind: "personal" | "company";
4
+ slug?: string;
5
+ phase: "warming" | "full";
6
+ }
7
+ export type EngineManifestScope = {
8
+ kind: "personal";
9
+ } | {
10
+ kind: "company";
11
+ slug: string;
12
+ };
13
+ export declare function engineOwnedManifestScopes(switches: Pick<VaultSyncSwitches, "allOff" | "personalOff" | "offCompanies"> | undefined, vaults: readonly EngineManifestVault[] | undefined): EngineManifestScope[];
14
+ export declare function manifestScopeArg(scope: EngineManifestScope): string;
15
+ //# sourceMappingURL=manifest-scopes.d.ts.map
@@ -0,0 +1,26 @@
1
+ export function engineOwnedManifestScopes(switches, vaults) {
2
+ if (!switches || switches.allOff)
3
+ return [];
4
+ let personal = false;
5
+ const companies = new Set();
6
+ for (const vault of vaults ?? []) {
7
+ if (vault.phase !== "full")
8
+ continue;
9
+ if (vault.kind === "personal") {
10
+ if (switches.personalOff)
11
+ personal = true;
12
+ continue;
13
+ }
14
+ const slug = vault.slug?.trim().toLowerCase();
15
+ if (slug && switches.offCompanies.has(slug))
16
+ companies.add(slug);
17
+ }
18
+ const scopes = personal ? [{ kind: "personal" }] : [];
19
+ for (const slug of [...companies].sort())
20
+ scopes.push({ kind: "company", slug });
21
+ return scopes;
22
+ }
23
+ export function manifestScopeArg(scope) {
24
+ return scope.kind === "personal" ? "personal" : scope.slug;
25
+ }
26
+ //# sourceMappingURL=manifest-scopes.js.map
@@ -1,4 +1,4 @@
1
- import { type SyncCorePlanFigures, type SyncCoreVault } from "./engine-home.js";
1
+ import { type SyncCoreVault } from "./engine-home.js";
2
2
  import { type SyncCoreBinaryInstall } from "./release.js";
3
3
  import { type SyncCoreRolloutState } from "./rollout-state.js";
4
4
  export interface SyncCoreMembership {
@@ -26,6 +26,6 @@ export interface SyncCoreReconcileResult {
26
26
  kept: string[];
27
27
  warnings: string[];
28
28
  }
29
- export declare function promotionHold(vault: SyncCoreVault, plan: SyncCorePlanFigures, release: string): string | undefined;
29
+ export declare function companyFullHold(vault: SyncCoreVault, release: string): string | undefined;
30
30
  export declare function reconcileSyncCore(deps: SyncCoreReconcileDeps): Promise<SyncCoreReconcileResult>;
31
31
  //# sourceMappingURL=reconcile.d.ts.map
@@ -2,28 +2,18 @@ import * as fs from "node:fs";
2
2
  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
- import { clearEngineStatus, engineReadOnlyPlan, removeEngineOwnership, syncCoreEngineHome, writeEngineHome, } from "./engine-home.js";
6
- import { SYNC_CORE_COMPANY_FULL_RELEASE, SYNC_CORE_PROMOTION_OP_LIMIT, SYNC_CORE_RELEASE, companyFullModeAllowed, installedSyncCoreRelease, syncCoreBinaryPath, } from "./release.js";
5
+ import { clearEngineStatus, engineFirstCycle, isSyncCoreVaultUid, removeEngineOwnership, syncCoreEngineHome, writeEngineHome, } from "./engine-home.js";
6
+ import { SYNC_CORE_COMPANY_FULL_RELEASE, SYNC_CORE_RELEASE, companyFullModeAllowed, installedSyncCoreRelease, syncCoreBinaryPath, } from "./release.js";
7
7
  import { readSyncCoreSkips } from "./skip.js";
8
- import { readSyncCoreRolloutState, writeSyncCoreRolloutState, } from "./rollout-state.js";
8
+ import { readSyncCoreRolloutState, syncCoreRolloutStatePath, writeSyncCoreRolloutState, } from "./rollout-state.js";
9
9
  function message(error) {
10
10
  return error instanceof Error ? error.message : String(error);
11
11
  }
12
- export function promotionHold(vault, plan, release) {
13
- if (vault.kind === "company" && !companyFullModeAllowed(release)) {
14
- const needed = SYNC_CORE_COMPANY_FULL_RELEASE ? `engine release ${SYNC_CORE_COMPANY_FULL_RELEASE}` : "an engine release that";
15
- return `company vaults stay read-only until ${needed} scopes the local tree to this account's visibility (${release} installed)`;
16
- }
17
- if (plan.deletes > SYNC_CORE_PROMOTION_OP_LIMIT) {
18
- return `the read-only plan would delete ${plan.deletes} paths (limit ${SYNC_CORE_PROMOTION_OP_LIMIT})`;
19
- }
20
- if (plan.conflicts > SYNC_CORE_PROMOTION_OP_LIMIT) {
21
- return `the read-only plan would write ${plan.conflicts} conflict or merge copies (limit ${SYNC_CORE_PROMOTION_OP_LIMIT})`;
22
- }
23
- if (plan.uploads > SYNC_CORE_PROMOTION_OP_LIMIT) {
24
- return `the read-only plan would upload ${plan.uploads} files against ${plan.remoteRows} server rows (limit ${SYNC_CORE_PROMOTION_OP_LIMIT})`;
25
- }
26
- return undefined;
12
+ export function companyFullHold(vault, release) {
13
+ if (vault.kind !== "company" || companyFullModeAllowed(release))
14
+ return undefined;
15
+ const needed = SYNC_CORE_COMPANY_FULL_RELEASE ? `engine release ${SYNC_CORE_COMPANY_FULL_RELEASE} or later` : "an engine release cleared for company vaults";
16
+ return `company vaults move to the new sync engine with ${needed} (${release} installed); staying on the current sync`;
27
17
  }
28
18
  function localCompanies(hqRoot, memberships) {
29
19
  const manifest = readCompaniesManifestMap(hqRoot) ?? {};
@@ -44,13 +34,41 @@ function localCompanies(hqRoot, memberships) {
44
34
  }
45
35
  return candidates;
46
36
  }
37
+ function ownedVaultsWithoutState(home, hqRoot) {
38
+ if (!fs.existsSync(syncCoreRolloutStatePath(home)))
39
+ return [];
40
+ const slugByUid = new Map();
41
+ for (const [slug, entry] of Object.entries(readCompaniesManifestMap(hqRoot) ?? {})) {
42
+ if (typeof entry?.cloud_uid === "string" && /^[a-z0-9][a-z0-9_-]*$/.test(slug))
43
+ slugByUid.set(entry.cloud_uid, slug);
44
+ }
45
+ let uids;
46
+ try {
47
+ uids = fs.readdirSync(path.join(home, ".hq", "sync-core", "engines")).filter(isSyncCoreVaultUid);
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ const vaults = [];
53
+ for (const uid of uids) {
54
+ const engineHome = syncCoreEngineHome(home, uid);
55
+ if (!fs.existsSync(path.join(engineHome, ".hq", "sync-core", "owned", `${uid}.json`)))
56
+ continue;
57
+ const slug = slugByUid.get(uid);
58
+ if (uid.startsWith("cmp_") && !slug)
59
+ continue;
60
+ vaults.push({ uid, kind: uid.startsWith("prs_") ? "personal" : "company", ...(slug ? { slug } : {}), phase: "full", engineHome });
61
+ }
62
+ return vaults;
63
+ }
47
64
  export async function reconcileSyncCore(deps) {
48
65
  const now = deps.now ?? (() => new Date());
49
66
  const log = deps.log ?? (() => { });
50
67
  const release = deps.release ?? SYNC_CORE_RELEASE;
51
68
  const warnings = [];
52
69
  const previous = readSyncCoreRolloutState(deps.home);
53
- const previousByUid = new Map((previous?.vaults ?? []).map((vault) => [vault.uid, vault]));
70
+ const previousVaults = previous?.vaults ?? ownedVaultsWithoutState(deps.home, deps.hqRoot);
71
+ const previousByUid = new Map(previousVaults.map((vault) => [vault.uid, vault]));
54
72
  const memberships = await deps.listMemberships();
55
73
  const desired = [];
56
74
  let anyCompanyOn = false;
@@ -111,6 +129,15 @@ export async function reconcileSyncCore(deps) {
111
129
  desired.length = 0;
112
130
  }
113
131
  }
132
+ const held = [];
133
+ const engineRelease = installedRelease ?? release;
134
+ for (let i = desired.length - 1; i >= 0; i -= 1) {
135
+ const reason = companyFullHold(desired[i], engineRelease);
136
+ if (!reason)
137
+ continue;
138
+ held.unshift({ uid: desired[i].uid, reason });
139
+ desired.splice(i, 1);
140
+ }
114
141
  const at = now().toISOString();
115
142
  const switchesBefore = readVaultSyncSwitches(deps.home);
116
143
  const vaults = [];
@@ -124,42 +151,36 @@ export async function reconcileSyncCore(deps) {
124
151
  const engineHome = syncCoreEngineHome(deps.home, vault.uid);
125
152
  if (!before)
126
153
  clearEngineStatus(deps.home, vault.uid);
127
- const plan = engineReadOnlyPlan(engineHome, vault.uid) ?? undefined;
128
- let phase = before?.phase === "full" ? "full" : "warming";
129
- let hold;
130
- if (phase === "warming" && plan) {
131
- hold = promotionHold(vault, plan, installedRelease ?? release);
132
- if (!hold)
133
- phase = "full";
134
- }
135
- writeEngineHome({ home: deps.home, hqRoot: deps.hqRoot, vault, phase, tokenFile: deps.tokenFile });
154
+ writeEngineHome({ home: deps.home, hqRoot: deps.hqRoot, vault, phase: "full", tokenFile: deps.tokenFile });
155
+ const wasFull = before?.phase === "full";
136
156
  const alreadyOff = vault.kind === "personal"
137
157
  ? switchesBefore.personalOff
138
158
  : switchesBefore.offCompanies.has(vault.slug ?? "");
139
- const legacyAlreadyOff = before?.phase === "full" ? before.legacyAlreadyOff === true : phase === "full" && alreadyOff;
140
- const legacyOffSince = phase === "full" ? before?.legacyOffSince ?? at : undefined;
159
+ const legacyAlreadyOff = wasFull ? before.legacyAlreadyOff === true : alreadyOff;
160
+ const legacyOffSince = (wasFull ? before.legacyOffSince : undefined) ?? at;
161
+ const recorded = wasFull ? before.firstCycle : undefined;
162
+ const firstCycle = recorded && recorded.planned > 0
163
+ ? recorded
164
+ : engineFirstCycle(engineHome, vault.uid, legacyOffSince) ?? recorded;
141
165
  vaults.push({
142
166
  uid: vault.uid,
143
167
  kind: vault.kind,
144
168
  ...(vault.slug ? { slug: vault.slug } : {}),
145
- phase,
169
+ phase: "full",
146
170
  engineHome,
147
- ...(legacyOffSince ? { legacyOffSince } : {}),
171
+ legacyOffSince,
148
172
  ...(legacyAlreadyOff ? { legacyAlreadyOff: true } : {}),
149
- ...(plan ? { plan } : {}),
150
- ...(hold ? { hold } : {}),
173
+ ...(firstCycle ? { firstCycle } : {}),
151
174
  });
152
- if (hold && before?.hold !== hold)
153
- log(`${vault.uid}: staying read-only: ${hold}`);
154
- if (phase === "full") {
155
- if (vault.kind === "company" && vault.slug)
156
- companies[vault.slug] = false;
157
- else
158
- personalOff = true;
159
- }
175
+ if (firstCycle && firstCycle !== recorded && (!recorded || firstCycle.planned > 0))
176
+ log(`${vault.uid}: first full cycle ${firstCycle.at}: ${firstCycle.planned} planned, ${firstCycle.completed} completed, ${firstCycle.held} held`);
177
+ if (vault.kind === "company" && vault.slug)
178
+ companies[vault.slug] = false;
179
+ else
180
+ personalOff = true;
160
181
  if (!before)
161
182
  started.push(vault.uid);
162
- else if (before.phase !== phase)
183
+ else if (!wasFull)
163
184
  promoted.push(vault.uid);
164
185
  else
165
186
  kept.push(vault.uid);
@@ -170,7 +191,7 @@ export async function reconcileSyncCore(deps) {
170
191
  if (desired.some((candidate) => candidate.uid === vault.uid))
171
192
  continue;
172
193
  stopped.push(vault.uid);
173
- if (vault.phase !== "full" || vault.legacyAlreadyOff)
194
+ if (vault.phase !== "full")
174
195
  continue;
175
196
  if (vault.kind === "company" && vault.slug)
176
197
  companies[vault.slug] = true;
@@ -184,6 +205,7 @@ export async function reconcileSyncCore(deps) {
184
205
  binaryPath,
185
206
  vaults,
186
207
  ...(skipped.length > 0 ? { skipped } : {}),
208
+ ...(held.length > 0 ? { held } : {}),
187
209
  ...(warnings.length > 0 ? { error: warnings.join("; ") } : {}),
188
210
  };
189
211
  writeSyncCoreRolloutState(deps.home, state);
@@ -203,13 +225,15 @@ export async function reconcileSyncCore(deps) {
203
225
  writeSyncCoreRolloutState(deps.home, state);
204
226
  }
205
227
  for (const uid of started)
206
- log(`${uid}: warming the new sync engine's index (home ${syncCoreEngineHome(deps.home, uid)})`);
228
+ log(`${uid}: moving to the new sync engine in full mode (home ${syncCoreEngineHome(deps.home, uid)})`);
207
229
  for (const uid of promoted)
208
- log(`${uid}: index warm; moving to the new sync engine`);
230
+ log(`${uid}: moving from the read-only index to full mode on the new sync engine`);
209
231
  for (const uid of stopped)
210
232
  log(`${uid}: back on the current sync`);
211
233
  for (const skip of skipped)
212
234
  log(`${skip.uid}: skipped on this machine: ${skip.reason}`);
235
+ for (const hold of held)
236
+ log(`${hold.uid}: ${hold.reason}`);
213
237
  for (const warning of warnings)
214
238
  log(`warning: ${warning}`);
215
239
  return { state, started, promoted, stopped, kept, warnings };
@@ -1,6 +1,5 @@
1
- export declare const SYNC_CORE_RELEASE = "v0.1.13";
1
+ export declare const SYNC_CORE_RELEASE = "v0.1.21";
2
2
  export declare const SYNC_CORE_COMPANY_FULL_RELEASE: string | null;
3
- export declare const SYNC_CORE_PROMOTION_OP_LIMIT = 100;
4
3
  export declare function compareSyncCoreReleases(a: string, b: string): number;
5
4
  export declare function companyFullModeAllowed(release: string): boolean;
6
5
  export declare const SYNC_CORE_DOWNLOAD_BASE = "https://indigoai-public.s3.amazonaws.com/hq-sync-core";
@@ -10,6 +9,7 @@ export declare function syncCoreReleaseMarkerPath(home?: string): string;
10
9
  export declare function syncCoreAssetName(release: string, platform: NodeJS.Platform, arch: string): string | null;
11
10
  export declare function syncCoreDownloadBase(env?: NodeJS.ProcessEnv): string;
12
11
  export declare function parseSha256Sums(text: string): Map<string, string>;
12
+ export declare function syncCoreBinaryVersion(binaryPath: string): string | null;
13
13
  export declare function installedSyncCoreRelease(home: string): string | null;
14
14
  export interface EnsureSyncCoreBinaryOptions {
15
15
  home: string;
@@ -3,9 +3,8 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
- export const SYNC_CORE_RELEASE = "v0.1.13";
7
- export const SYNC_CORE_COMPANY_FULL_RELEASE = null;
8
- export const SYNC_CORE_PROMOTION_OP_LIMIT = 100;
6
+ export const SYNC_CORE_RELEASE = "v0.1.21";
7
+ export const SYNC_CORE_COMPANY_FULL_RELEASE = "v0.1.21";
9
8
  export function compareSyncCoreReleases(a, b) {
10
9
  const parse = (release) => release.replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10) || 0);
11
10
  const [x, y] = [parse(a), parse(b)];
@@ -50,14 +49,27 @@ export function parseSha256Sums(text) {
50
49
  }
51
50
  return sums;
52
51
  }
52
+ export function syncCoreBinaryVersion(binaryPath) {
53
+ try {
54
+ const out = execFileSync(binaryPath, ["--version"], { encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"] });
55
+ const match = /hq-syncd\s+v?(\d+\.\d+\.\d+)/.exec(out);
56
+ return match ? `v${match[1]}` : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
53
62
  export function installedSyncCoreRelease(home) {
54
63
  try {
55
- const marker = fs.readFileSync(syncCoreReleaseMarkerPath(home), "utf8").trim();
56
64
  const binary = syncCoreBinaryPath(home);
57
- if (!marker || !fs.statSync(binary).isFile())
65
+ if (!fs.statSync(binary).isFile())
58
66
  return null;
59
67
  fs.accessSync(binary, fs.constants.X_OK);
60
- return marker;
68
+ const reported = syncCoreBinaryVersion(binary);
69
+ if (reported)
70
+ return reported;
71
+ const marker = fs.readFileSync(syncCoreReleaseMarkerPath(home), "utf8").trim();
72
+ return marker || null;
61
73
  }
62
74
  catch {
63
75
  return null;
@@ -96,6 +108,19 @@ export async function ensureSyncCoreBinary(opts) {
96
108
  const binaryPath = syncCoreBinaryPath(opts.home);
97
109
  const installedRelease = installedSyncCoreRelease(opts.home);
98
110
  if (installedRelease && compareSyncCoreReleases(installedRelease, release) >= 0 && fs.existsSync(binaryPath)) {
111
+ const marker = syncCoreReleaseMarkerPath(opts.home);
112
+ let current = "";
113
+ try {
114
+ current = fs.readFileSync(marker, "utf8").trim();
115
+ }
116
+ catch {
117
+ current = "";
118
+ }
119
+ if (current !== installedRelease) {
120
+ const tmp = `${marker}.${process.pid}.tmp`;
121
+ fs.writeFileSync(tmp, `${installedRelease}\n`, { mode: 0o600 });
122
+ fs.renameSync(tmp, marker);
123
+ }
99
124
  return { binaryPath, release: installedRelease, installed: false };
100
125
  }
101
126
  const asset = syncCoreAssetName(release, opts.platform, opts.arch);
@@ -1,4 +1,4 @@
1
- import { type SyncCorePlanFigures, type SyncCoreVaultKind, type SyncCoreVaultPhase } from "./engine-home.js";
1
+ import { type SyncCoreFirstCycle, type SyncCoreVaultKind, type SyncCoreVaultPhase } from "./engine-home.js";
2
2
  export interface RolloutVault {
3
3
  uid: string;
4
4
  kind: SyncCoreVaultKind;
@@ -7,8 +7,7 @@ export interface RolloutVault {
7
7
  engineHome: string;
8
8
  legacyOffSince?: string;
9
9
  legacyAlreadyOff?: true;
10
- plan?: SyncCorePlanFigures;
11
- hold?: string;
10
+ firstCycle?: SyncCoreFirstCycle;
12
11
  }
13
12
  export interface SyncCoreRolloutState {
14
13
  version: 1;
@@ -20,6 +19,10 @@ export interface SyncCoreRolloutState {
20
19
  uid: string;
21
20
  reason: string;
22
21
  }[];
22
+ held?: {
23
+ uid: string;
24
+ reason: string;
25
+ }[];
23
26
  error?: string;
24
27
  }
25
28
  export declare function syncCoreRolloutStatePath(home: string): string;
@@ -1,26 +1,29 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isSyncCoreVaultUid } from "./engine-home.js";
4
- function planFigures(value) {
4
+ const FIRST_CYCLE_COUNTS = [
5
+ "planned",
6
+ "completed",
7
+ "pending",
8
+ "held",
9
+ "notApplied",
10
+ "uploads",
11
+ "downloads",
12
+ "deletes",
13
+ "conflicts",
14
+ "deferred",
15
+ "listed",
16
+ ];
17
+ function firstCycle(value) {
5
18
  if (!value || typeof value !== "object" || Array.isArray(value))
6
19
  return null;
7
- const p = value;
8
- if (typeof p.at !== "string")
20
+ const c = value;
21
+ if (typeof c.at !== "string" || typeof c.truncated !== "boolean")
9
22
  return null;
10
- const numbers = ["uploads", "downloads", "deletes", "remoteRows", "localFiles"];
11
- if (numbers.some((key) => typeof p[key] !== "number" || !Number.isFinite(p[key])))
23
+ if (FIRST_CYCLE_COUNTS.some((key) => typeof c[key] !== "number" || !Number.isFinite(c[key])))
12
24
  return null;
13
- if (p.conflicts !== undefined && (typeof p.conflicts !== "number" || !Number.isFinite(p.conflicts)))
14
- return null;
15
- return {
16
- at: p.at,
17
- uploads: p.uploads,
18
- downloads: p.downloads,
19
- deletes: p.deletes,
20
- conflicts: typeof p.conflicts === "number" ? p.conflicts : 0,
21
- remoteRows: p.remoteRows,
22
- localFiles: p.localFiles,
23
- };
25
+ const counts = Object.fromEntries(FIRST_CYCLE_COUNTS.map((key) => [key, c[key]]));
26
+ return { at: c.at, ...counts, truncated: c.truncated };
24
27
  }
25
28
  function skippedEntries(value) {
26
29
  if (value === undefined)
@@ -57,10 +60,8 @@ function vault(value) {
57
60
  return null;
58
61
  if (v.kind === "company" && (typeof v.slug !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(v.slug)))
59
62
  return null;
60
- const plan = v.plan === undefined ? undefined : planFigures(v.plan);
61
- if (plan === null)
62
- return null;
63
- if (v.hold !== undefined && typeof v.hold !== "string")
63
+ const cycle = v.firstCycle === undefined ? undefined : firstCycle(v.firstCycle);
64
+ if (cycle === null)
64
65
  return null;
65
66
  return {
66
67
  uid: v.uid,
@@ -70,8 +71,7 @@ function vault(value) {
70
71
  engineHome: v.engineHome,
71
72
  ...(typeof v.legacyOffSince === "string" ? { legacyOffSince: v.legacyOffSince } : {}),
72
73
  ...(v.legacyAlreadyOff === true ? { legacyAlreadyOff: true } : {}),
73
- ...(plan ? { plan } : {}),
74
- ...(typeof v.hold === "string" ? { hold: v.hold } : {}),
74
+ ...(cycle ? { firstCycle: cycle } : {}),
75
75
  };
76
76
  }
77
77
  export function readSyncCoreRolloutState(home) {
@@ -95,6 +95,9 @@ export function readSyncCoreRolloutState(home) {
95
95
  const skipped = skippedEntries(obj.skipped);
96
96
  if (skipped === null)
97
97
  return null;
98
+ const held = skippedEntries(obj.held);
99
+ if (held === null)
100
+ return null;
98
101
  return {
99
102
  version: 1,
100
103
  readAt: obj.readAt,
@@ -102,6 +105,7 @@ export function readSyncCoreRolloutState(home) {
102
105
  binaryPath: obj.binaryPath,
103
106
  vaults: vaults,
104
107
  ...(skipped.length > 0 ? { skipped } : {}),
108
+ ...(held.length > 0 ? { held } : {}),
105
109
  ...(typeof obj.error === "string" ? { error: obj.error } : {}),
106
110
  };
107
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.175",
3
+ "version": "5.345.177",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -52,7 +52,7 @@
52
52
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
53
53
  "@aws-sdk/client-s3": "^3.1049.0",
54
54
  "@aws-sdk/dsql-signer": "^3.1146.0",
55
- "@indigoai-us/hq-cloud": "6.18.103",
55
+ "@indigoai-us/hq-cloud": "6.18.104",
56
56
  "@indigoai-us/hq-flags-client": "^0.1.2",
57
57
  "@modelcontextprotocol/sdk": "1.29.0",
58
58
  "@sentry/node": "^10.49.0",
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.175",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.175",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.175",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.175",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.175",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.175"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.177",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.177",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.177",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.177",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.177",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.177"
123
123
  }
124
124
  }