@indigoai-us/hq-cli 5.212.0 → 5.214.0
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 +24 -0
- package/dist/commands/cloud.d.ts +13 -1
- package/dist/commands/cloud.js +24 -3
- package/dist/commands/lanes.js +10 -7
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +4 -0
- package/dist/lib/doctor/checks/sync-runtime.d.ts +11 -0
- package/dist/lib/doctor/checks/sync-runtime.js +166 -0
- package/dist/lib/doctor/client-health-sentry.d.ts +1 -1
- package/dist/lib/doctor/client-health-sentry.js +7 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +5 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.214.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq doctor` now checks the last hour of the sync runner log for stuck
|
|
10
|
+
realtime lag, cloud permission refusals, runner restart loops, device-limit
|
|
11
|
+
backoff, and repeated conflicts. Client health reports send these to Sentry
|
|
12
|
+
as reason codes and counts only.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- The desktop app's first sync push now uploads files inside private folders.
|
|
17
|
+
The credentials the app hands to `hq sync push --creds-from-stdin` cannot
|
|
18
|
+
write there directly, so the push also passes the vault config to hq-cloud
|
|
19
|
+
6.18.5, which sends those files through presigned uploads.
|
|
20
|
+
|
|
21
|
+
## [5.213.0] — 2026-09-26
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `hq lanes accounts set-home` and `login` return
|
|
26
|
+
`company_selection_failed` for company-selection errors without a stack
|
|
27
|
+
trace. Other resolver failures remain `internal` and include a stack trace.
|
|
28
|
+
|
|
5
29
|
## [5.212.0] — 2026-09-26
|
|
6
30
|
|
|
7
31
|
### Changed
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
import { Command } from "commander";
|
|
16
|
-
import { type ConflictStrategy, type MembershipSyncConfig, type SyncMode, type PullScope, type ExplicitGrant } from "@indigoai-us/hq-cloud";
|
|
16
|
+
import { type ConflictStrategy, type EntityContext, type MembershipSyncConfig, type SyncMode, type PullScope, type ExplicitGrant, type ShareOptions, type VaultServiceConfig } from "@indigoai-us/hq-cloud";
|
|
17
17
|
import { type BannerLevel } from "../lib/narrow-hint-banner.js";
|
|
18
18
|
/**
|
|
19
19
|
* Terminal-failure signal for the sync fanout runners. The runners
|
|
@@ -264,6 +264,18 @@ export declare function pullAll(options: PullAllOptions, deps: PullAllDeps): Pro
|
|
|
264
264
|
* deps so tests can drive it without network or filesystem.
|
|
265
265
|
*/
|
|
266
266
|
export declare function pushAll(options: PushAllOptions, deps: PushAllDeps): Promise<PushAllResult>;
|
|
267
|
+
/**
|
|
268
|
+
* Credentials `hq sync push` hands to share(). A pre-vended EntityContext
|
|
269
|
+
* (AppBar's `--creds-from-stdin` first push) cannot write under private
|
|
270
|
+
* folders directly, so it travels with the caller's vault config as
|
|
271
|
+
* `privateFolderVaultConfig`. share() takes exactly one of `vaultConfig` /
|
|
272
|
+
* `entityContext`.
|
|
273
|
+
*/
|
|
274
|
+
export declare function pushShareCredentials(input: {
|
|
275
|
+
entityContext?: EntityContext;
|
|
276
|
+
vaultConfig?: VaultServiceConfig;
|
|
277
|
+
scopeVaultConfig?: VaultServiceConfig;
|
|
278
|
+
}): Pick<ShareOptions, "entityContext" | "vaultConfig" | "privateFolderVaultConfig">;
|
|
267
279
|
/**
|
|
268
280
|
* Resolve the canonical person entity UID for the logged-in user. Used by
|
|
269
281
|
* `hq sync {push,pull,now} --personal` to target the personal vault without
|
package/dist/commands/cloud.js
CHANGED
|
@@ -455,6 +455,24 @@ export async function pushAll(options, deps) {
|
|
|
455
455
|
}
|
|
456
456
|
return result;
|
|
457
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Credentials `hq sync push` hands to share(). A pre-vended EntityContext
|
|
460
|
+
* (AppBar's `--creds-from-stdin` first push) cannot write under private
|
|
461
|
+
* folders directly, so it travels with the caller's vault config as
|
|
462
|
+
* `privateFolderVaultConfig`. share() takes exactly one of `vaultConfig` /
|
|
463
|
+
* `entityContext`.
|
|
464
|
+
*/
|
|
465
|
+
export function pushShareCredentials(input) {
|
|
466
|
+
if (input.entityContext) {
|
|
467
|
+
return {
|
|
468
|
+
entityContext: input.entityContext,
|
|
469
|
+
...(input.scopeVaultConfig
|
|
470
|
+
? { privateFolderVaultConfig: input.scopeVaultConfig }
|
|
471
|
+
: {}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return input.vaultConfig ? { vaultConfig: input.vaultConfig } : {};
|
|
475
|
+
}
|
|
458
476
|
/**
|
|
459
477
|
* Resolve the canonical person entity UID for the logged-in user. Used by
|
|
460
478
|
* `hq sync {push,pull,now} --personal` to target the personal vault without
|
|
@@ -715,7 +733,7 @@ export function registerCloudCommands(program) {
|
|
|
715
733
|
// 2. default: vend via cached Cognito session (the human CLI path).
|
|
716
734
|
let entityContext;
|
|
717
735
|
let vaultConfig;
|
|
718
|
-
/** Vault API config for membership/grant scope lookups
|
|
736
|
+
/** Vault API config for membership/grant scope lookups and, on the pre-vended `--creds-from-stdin` path, private-folder uploads. */
|
|
719
737
|
let scopeVaultConfig;
|
|
720
738
|
if (options.credsFromStdin) {
|
|
721
739
|
if (process.stdin.isTTY) {
|
|
@@ -798,8 +816,11 @@ export function registerCloudCommands(program) {
|
|
|
798
816
|
company: targetCompany,
|
|
799
817
|
message: options.message,
|
|
800
818
|
onConflict: options.onConflict,
|
|
801
|
-
|
|
802
|
-
|
|
819
|
+
...pushShareCredentials({
|
|
820
|
+
entityContext,
|
|
821
|
+
vaultConfig,
|
|
822
|
+
scopeVaultConfig,
|
|
823
|
+
}),
|
|
803
824
|
hqRoot: options.hqRoot,
|
|
804
825
|
onEvent,
|
|
805
826
|
...(personalMode ? { personalMode: true } : {}),
|
package/dist/commands/lanes.js
CHANGED
|
@@ -121,15 +121,18 @@ function printJson(value) {
|
|
|
121
121
|
console.log(JSON.stringify(withoutProviderHome(value), null, 2));
|
|
122
122
|
}
|
|
123
123
|
function failJson(err) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
const commandError = err instanceof CompanySelectionError
|
|
125
|
+
? new LanesError("company_selection_failed", err.message)
|
|
126
|
+
: err;
|
|
127
|
+
if (isLanesError(commandError)) {
|
|
128
|
+
if (commandError.error === "hire_grant_exhausted") {
|
|
129
|
+
process.stderr.write(`hq lanes: ${commandError.message}\n`);
|
|
130
|
+
}
|
|
131
|
+
printJson({ ...commandError.toJSON(), message: commandError.message });
|
|
129
132
|
process.exit(1);
|
|
130
133
|
}
|
|
131
|
-
const message =
|
|
132
|
-
process.stderr.write(`hq lanes: ${message}${
|
|
134
|
+
const message = commandError instanceof Error ? commandError.message : String(commandError);
|
|
135
|
+
process.stderr.write(`hq lanes: ${message}${commandError instanceof Error && commandError.stack ? `\n${commandError.stack}` : ""}\n`);
|
|
133
136
|
printJson({ error: "internal", message });
|
|
134
137
|
process.exit(1);
|
|
135
138
|
}
|
|
@@ -64,6 +64,8 @@ export interface SyncManifestUploadStatus {
|
|
|
64
64
|
}
|
|
65
65
|
/** Injectable dependencies — defaults are the real collectors. */
|
|
66
66
|
export interface SyncHealthDeps {
|
|
67
|
+
/** Runner log and conflict signals; omitted in unit tests of other checks. */
|
|
68
|
+
runtime?: (now: Date) => CheckResult[];
|
|
67
69
|
versions: (hqRoot: string) => SyncVersionInfo;
|
|
68
70
|
journals: () => readonly SyncJournalSummary[];
|
|
69
71
|
now: () => Date;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* Every dependency is injectable so the family is unit-testable without an HQ
|
|
24
24
|
* tree, a state dir, or the wall clock.
|
|
25
25
|
*/
|
|
26
|
+
import { defaultSyncRuntimeResults } from "./sync-runtime.js";
|
|
26
27
|
import * as fs from "node:fs";
|
|
27
28
|
import * as path from "node:path";
|
|
28
29
|
// Namespace import, deliberately: hq-cloud is an ESM package, so a static
|
|
@@ -50,6 +51,7 @@ export const UPDATE_CACHE_RELPATH = path.join("workspace", ".hq-update-check", "
|
|
|
50
51
|
function withDefaults(deps, hqRoot) {
|
|
51
52
|
return {
|
|
52
53
|
...deps,
|
|
54
|
+
runtime: deps.runtime ?? (() => []),
|
|
53
55
|
manifestStatuses: deps.manifestStatuses ?? defaultManifestStatuses,
|
|
54
56
|
manifestAvailable: deps.manifestAvailable ?? (() => loadManifestExports() !== null),
|
|
55
57
|
unresolvedScopes: deps.unresolvedScopes ?? defaultUnresolvedScopes,
|
|
@@ -118,6 +120,7 @@ const DEFAULT_DEPS = {
|
|
|
118
120
|
journals: () => hqCloud.listJournals(),
|
|
119
121
|
now: () => new Date(),
|
|
120
122
|
manifestStatuses: defaultManifestStatuses,
|
|
123
|
+
runtime: defaultSyncRuntimeResults,
|
|
121
124
|
};
|
|
122
125
|
/**
|
|
123
126
|
* Map the machine's journal shards onto the manifest scopes hq-cloud keys its
|
|
@@ -190,6 +193,7 @@ export function checkSyncHealth(context, rawDeps = DEFAULT_DEPS) {
|
|
|
190
193
|
...versionResults(context, deps),
|
|
191
194
|
...updateAvailabilityResult(context, deps),
|
|
192
195
|
...journalAndManifestResults(deps),
|
|
196
|
+
...deps.runtime(deps.now()),
|
|
193
197
|
];
|
|
194
198
|
}
|
|
195
199
|
catch (error) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CheckResult } from "../types.js";
|
|
2
|
+
export declare const SYNC_RUNTIME_WINDOW_MS: number;
|
|
3
|
+
export interface SyncRuntimeOptions {
|
|
4
|
+
now: number;
|
|
5
|
+
conflictCount?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Classify the last hour of sync log lines into privacy-safe results. */
|
|
8
|
+
export declare function analyzeSyncRuntimeLog(lines: readonly string[], options: SyncRuntimeOptions): CheckResult[];
|
|
9
|
+
/** Read the machine's sync logs and client health file. */
|
|
10
|
+
export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string): CheckResult[];
|
|
11
|
+
//# sourceMappingURL=sync-runtime.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync runtime checks: problems visible only in the running sync runner's log
|
|
3
|
+
* (realtime lag, receiver permission errors, restart loops, device-limit
|
|
4
|
+
* backoff) plus the conflict counter the desktop app keeps in client health.
|
|
5
|
+
*
|
|
6
|
+
* Results carry only a closed reason code and counts. Log content (paths,
|
|
7
|
+
* tenant ids, AWS error prose) never leaves this module.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
export const SYNC_RUNTIME_WINDOW_MS = 60 * 60_000;
|
|
13
|
+
const LAG_SECONDS = 15 * 60;
|
|
14
|
+
const LAG_MIN_RECEIPTS = 3;
|
|
15
|
+
const RESTART_MIN_STARTS = 4;
|
|
16
|
+
const CONFLICT_MIN_COUNT = 5;
|
|
17
|
+
const LOG_TAIL_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const TIMESTAMP = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\s/;
|
|
19
|
+
const PERMISSION = /AccessDenied|not authorized to perform|\b403\b/;
|
|
20
|
+
const DEVICE_LIMIT = /sync device limit exceeded|"sync_device_limit"/;
|
|
21
|
+
function parseRecord(rest) {
|
|
22
|
+
const start = rest.indexOf("{");
|
|
23
|
+
if (start < 0)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const value = JSON.parse(rest.slice(start));
|
|
27
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
28
|
+
? value
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Classify the last hour of sync log lines into privacy-safe results. */
|
|
36
|
+
export function analyzeSyncRuntimeLog(lines, options) {
|
|
37
|
+
const since = options.now - SYNC_RUNTIME_WINDOW_MS;
|
|
38
|
+
let slowReceipts = 0;
|
|
39
|
+
let denied = 0;
|
|
40
|
+
let starts = 0;
|
|
41
|
+
let deviceLimited = 0;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const stamp = TIMESTAMP.exec(line);
|
|
44
|
+
if (!stamp)
|
|
45
|
+
continue;
|
|
46
|
+
const time = Date.parse(stamp[1]);
|
|
47
|
+
if (!Number.isFinite(time) || time < since || time > options.now)
|
|
48
|
+
continue;
|
|
49
|
+
const rest = line.slice(stamp[0].length);
|
|
50
|
+
if (DEVICE_LIMIT.test(rest))
|
|
51
|
+
deviceLimited += 1;
|
|
52
|
+
const record = parseRecord(rest);
|
|
53
|
+
if (!record)
|
|
54
|
+
continue;
|
|
55
|
+
const event = record.event;
|
|
56
|
+
if (event === "fanout.receive") {
|
|
57
|
+
const latency = record.latencySeconds;
|
|
58
|
+
if (typeof latency === "number" && latency > LAG_SECONDS)
|
|
59
|
+
slowReceipts += 1;
|
|
60
|
+
}
|
|
61
|
+
else if (event === "receiver.start") {
|
|
62
|
+
starts += 1;
|
|
63
|
+
}
|
|
64
|
+
if ((record.type === "receiver" || record.type === "error") &&
|
|
65
|
+
PERMISSION.test(JSON.stringify(record.err ?? record.message ?? ""))) {
|
|
66
|
+
denied += 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const conflicts = options.conflictCount ?? 0;
|
|
70
|
+
const checks = [
|
|
71
|
+
{ id: "realtime-lag", reasonCode: "realtime-lag-stuck", failing: slowReceipts >= LAG_MIN_RECEIPTS, count: slowReceipts, label: "incoming changes delayed over 15 minutes" },
|
|
72
|
+
{ id: "receiver-permission", reasonCode: "receiver-permission-denied", failing: denied > 0, count: denied, label: "cloud permission refusals" },
|
|
73
|
+
{ id: "runner-restarts", reasonCode: "runner-restart-loop", failing: starts >= RESTART_MIN_STARTS, count: starts, label: "sync runner starts" },
|
|
74
|
+
{ id: "device-limit", reasonCode: "device-limit-backoff", failing: deviceLimited > 0, count: deviceLimited, label: "device-limit backoffs" },
|
|
75
|
+
{ id: "conflicts", reasonCode: "repeated-conflicts", failing: conflicts >= CONFLICT_MIN_COUNT, count: conflicts, label: "unresolved conflicts" },
|
|
76
|
+
];
|
|
77
|
+
return checks.map((check) => check.failing
|
|
78
|
+
? {
|
|
79
|
+
status: "WARN",
|
|
80
|
+
checkId: `sync.runtime.${check.id}`,
|
|
81
|
+
reasonCode: check.reasonCode,
|
|
82
|
+
message: `${check.count} ${check.label} in the last hour.`,
|
|
83
|
+
remediation: "Run `hq sync status` and check ~/.hq/logs/hq-sync.log.",
|
|
84
|
+
}
|
|
85
|
+
: {
|
|
86
|
+
status: "PASS",
|
|
87
|
+
checkId: `sync.runtime.${check.id}`,
|
|
88
|
+
message: `${check.count} ${check.label} in the last hour.`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const LOG_GENERATIONS = 4;
|
|
92
|
+
function readLogTail(file) {
|
|
93
|
+
const fd = fs.openSync(file, "r");
|
|
94
|
+
try {
|
|
95
|
+
const size = fs.fstatSync(fd).size;
|
|
96
|
+
const length = Math.min(size, LOG_TAIL_BYTES);
|
|
97
|
+
const buffer = Buffer.alloc(length);
|
|
98
|
+
fs.readSync(fd, buffer, 0, length, size - length);
|
|
99
|
+
return buffer.toString("utf8").split("\n");
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
fs.closeSync(fd);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function firstTimestamp(lines) {
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const stamp = TIMESTAMP.exec(line);
|
|
108
|
+
if (stamp)
|
|
109
|
+
return Date.parse(stamp[1]);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Lines from the active log and as many rotated generations as needed to
|
|
115
|
+
* cover the window. Throws when a present log cannot be read.
|
|
116
|
+
*/
|
|
117
|
+
function readWindowLines(logFile, since) {
|
|
118
|
+
const generations = [];
|
|
119
|
+
for (let n = 0; n < LOG_GENERATIONS; n += 1) {
|
|
120
|
+
const file = n === 0 ? logFile : `${logFile}.${n}`;
|
|
121
|
+
if (!fs.existsSync(file))
|
|
122
|
+
break;
|
|
123
|
+
const lines = readLogTail(file);
|
|
124
|
+
generations.unshift(lines);
|
|
125
|
+
const first = firstTimestamp(lines);
|
|
126
|
+
if (first !== null && first <= since)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return generations.length > 0 ? generations.flat() : null;
|
|
130
|
+
}
|
|
131
|
+
function readConflictCount(file) {
|
|
132
|
+
try {
|
|
133
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
134
|
+
const count = value?.conflictCount;
|
|
135
|
+
return typeof count === "number" && Number.isFinite(count) ? count : 0;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Read the machine's sync logs and client health file. */
|
|
142
|
+
export function defaultSyncRuntimeResults(now, hqHome = path.join(os.homedir(), ".hq")) {
|
|
143
|
+
const options = {
|
|
144
|
+
now: now.getTime(),
|
|
145
|
+
conflictCount: readConflictCount(path.join(hqHome, "client-health.json")),
|
|
146
|
+
};
|
|
147
|
+
const logFile = path.join(hqHome, "logs", "hq-sync.log");
|
|
148
|
+
let lines;
|
|
149
|
+
try {
|
|
150
|
+
lines = readWindowLines(logFile, options.now - SYNC_RUNTIME_WINDOW_MS);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return [
|
|
154
|
+
{ status: "UNKNOWN", checkId: "sync.runtime.log", message: "The sync runner log could not be read." },
|
|
155
|
+
...analyzeSyncRuntimeLog([], options).filter((r) => r.checkId === "sync.runtime.conflicts"),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
if (lines === null) {
|
|
159
|
+
return [
|
|
160
|
+
{ status: "NA", checkId: "sync.runtime.log", message: "No sync runner log on this machine." },
|
|
161
|
+
...analyzeSyncRuntimeLog([], options).filter((r) => r.checkId === "sync.runtime.conflicts"),
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
return analyzeSyncRuntimeLog(lines, options);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=sync-runtime.js.map
|
|
@@ -5,7 +5,7 @@ export declare const CLIENT_HEALTH_INSTALL_ID_FILENAME = "install-id";
|
|
|
5
5
|
export declare const CLIENT_HEALTH_LAST_RESULT_FILENAME = "last-result.json";
|
|
6
6
|
export type ClientHealthFixOutcome = "not_auto_fixable" | "fix_failed" | "fix_timed_out";
|
|
7
7
|
export type ClientHealthStatus = "FAIL" | "WARN";
|
|
8
|
-
export type ClientHealthCheckClass = "sync.update.core" | "sync.journal.personal" | "sync.journal.personal_vault" | "sync.journal.company" | "sync.manifest.personal" | "sync.manifest.personal_vault" | "sync.manifest.company" | "other";
|
|
8
|
+
export type ClientHealthCheckClass = "sync.update.core" | "sync.journal.personal" | "sync.journal.personal_vault" | "sync.journal.company" | "sync.manifest.personal" | "sync.manifest.personal_vault" | "sync.manifest.company" | "sync.runtime" | "other";
|
|
9
9
|
export interface ClientHealthSentryEvent {
|
|
10
10
|
message: string;
|
|
11
11
|
level: "warning";
|
|
@@ -24,6 +24,11 @@ const REASON_CODES = new Set([
|
|
|
24
24
|
"never-uploaded",
|
|
25
25
|
"invalid-upload-time",
|
|
26
26
|
"unresolved-company-uid",
|
|
27
|
+
"realtime-lag-stuck",
|
|
28
|
+
"receiver-permission-denied",
|
|
29
|
+
"runner-restart-loop",
|
|
30
|
+
"device-limit-backoff",
|
|
31
|
+
"repeated-conflicts",
|
|
27
32
|
]);
|
|
28
33
|
const FIX_OUTCOMES = new Set(["not_auto_fixable", "fix_failed", "fix_timed_out"]);
|
|
29
34
|
const INSTALL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -52,6 +57,8 @@ function cleanArchitecture(value) {
|
|
|
52
57
|
export function normalizeClientHealthCheckId(checkId) {
|
|
53
58
|
if (checkId === "sync.update.core")
|
|
54
59
|
return checkId;
|
|
60
|
+
if (checkId === "sync.runtime" || checkId.startsWith("sync.runtime."))
|
|
61
|
+
return "sync.runtime";
|
|
55
62
|
if (checkId === "sync.journal.__hq_personal_vault__")
|
|
56
63
|
return "sync.journal.personal_vault";
|
|
57
64
|
if (checkId === "sync.manifest.__hq_personal_vault__")
|
|
@@ -19,7 +19,7 @@ export declare const DOCTOR_STATUSES: readonly ["PASS", "FAIL", "WARN", "UNTESTE
|
|
|
19
19
|
*/
|
|
20
20
|
export type DoctorStatus = (typeof DOCTOR_STATUSES)[number];
|
|
21
21
|
/** Stable, privacy-safe diagnostics emitted by the sync health family. */
|
|
22
|
-
export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "never-synced", "invalid-last-sync", "stale-threshold", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid"];
|
|
22
|
+
export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "never-synced", "invalid-last-sync", "stale-threshold", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid", "realtime-lag-stuck", "receiver-permission-denied", "runner-restart-loop", "device-limit-backoff", "repeated-conflicts"];
|
|
23
23
|
export type SyncReasonCode = (typeof SYNC_REASON_CODES)[number];
|
|
24
24
|
/** Runtime type guard: true only for a member of the fixed vocabulary. */
|
|
25
25
|
export declare function isDoctorStatus(value: unknown): value is DoctorStatus;
|
package/dist/lib/doctor/types.js
CHANGED
|
@@ -35,6 +35,11 @@ export const SYNC_REASON_CODES = [
|
|
|
35
35
|
"never-uploaded",
|
|
36
36
|
"invalid-upload-time",
|
|
37
37
|
"unresolved-company-uid",
|
|
38
|
+
"realtime-lag-stuck",
|
|
39
|
+
"receiver-permission-denied",
|
|
40
|
+
"runner-restart-loop",
|
|
41
|
+
"device-limit-backoff",
|
|
42
|
+
"repeated-conflicts",
|
|
38
43
|
];
|
|
39
44
|
/** Runtime type guard: true only for a member of the fixed vocabulary. */
|
|
40
45
|
export function isDoctorStatus(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.214.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
43
43
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
44
|
-
"@indigoai-us/hq-cloud": "~6.
|
|
44
|
+
"@indigoai-us/hq-cloud": "~6.18.5",
|
|
45
45
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
46
46
|
"@sentry/node": "^10.49.0",
|
|
47
47
|
"@tobilu/qmd": "2.5.3",
|