@bli-cockpit/cli 0.2.4 → 0.2.5
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/README.md +8 -0
- package/dist/commands/local.js +126 -27
- package/dist/local-state.js +11 -1
- package/dist/spool/install-event-outbox.js +191 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,9 @@ Local files:
|
|
|
160
160
|
- `~/.config/bli-cockpit/config.json`: dashboard URL, device label, collection roots.
|
|
161
161
|
- `~/.config/bli-cockpit/session.json`: paired device token and owner metadata.
|
|
162
162
|
- `~/.local/state/bli-cockpit/spool/`: safe retry records.
|
|
163
|
+
- `~/.local/state/bli-cockpit/spool/install-events/`: private atomic collector
|
|
164
|
+
health receipts waiting for authenticated delivery. Receipts contain only
|
|
165
|
+
sanitized operation metadata, never command output or transcript content.
|
|
163
166
|
- `~/.local/state/bli-cockpit/cursors/`: upload/backfill cursors, no raw content.
|
|
164
167
|
- `.codex-autorunner/contextspace/active_context.md`: current work context inside a repo.
|
|
165
168
|
|
|
@@ -180,6 +183,11 @@ cockpit status --workspace "$PWD" --json
|
|
|
180
183
|
cockpit sync --workspace "$PWD" --json
|
|
181
184
|
```
|
|
182
185
|
|
|
186
|
+
`status --json` includes `pending_health_receipt_count`,
|
|
187
|
+
`oldest_pending_health_receipt_at`, and
|
|
188
|
+
`last_health_receipt_failure_reason`. A nonzero count means Cockpit preserved a
|
|
189
|
+
collector failure locally and will replay it on the next authenticated run.
|
|
190
|
+
|
|
183
191
|
If `cockpit update` fails with npm `EACCES`, fix Homebrew global-package ownership once:
|
|
184
192
|
|
|
185
193
|
```bash
|
package/dist/commands/local.js
CHANGED
|
@@ -18,6 +18,7 @@ import { discoverGitWorktrees } from "../repo-identity.js";
|
|
|
18
18
|
import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
19
19
|
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
20
20
|
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
21
|
+
import { enqueueInstallEventEntry, readPendingInstallEventEntries, recordInstallEventAttemptFailure, removeInstallEventEntry, } from "../spool/install-event-outbox.js";
|
|
21
22
|
export const rootCommandNames = new Set([
|
|
22
23
|
"onboard",
|
|
23
24
|
"update",
|
|
@@ -671,6 +672,28 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
671
672
|
if (options.events.length === 0)
|
|
672
673
|
return;
|
|
673
674
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
675
|
+
try {
|
|
676
|
+
await enqueueInstallEventEntry(paths, {
|
|
677
|
+
dashboardUrl: options.dashboardUrl,
|
|
678
|
+
cliVersion: LOCAL_COLLECTOR_VERSION,
|
|
679
|
+
command: options.command,
|
|
680
|
+
osPlatform: os.platform(),
|
|
681
|
+
events: options.events.map((event) => ({
|
|
682
|
+
step: event.step.trim().slice(0, 120),
|
|
683
|
+
status: event.status,
|
|
684
|
+
...(event.error_code
|
|
685
|
+
? { error_code: sanitizeInstallErrorCode(event.error_code) }
|
|
686
|
+
: {}),
|
|
687
|
+
...(event.at ? { at: event.at } : {}),
|
|
688
|
+
})),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
if (options.json) {
|
|
693
|
+
writeLine(options.io.stderr, "Install event outbox unavailable: local_write_failed");
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
674
697
|
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
675
698
|
if (!session ||
|
|
676
699
|
session.session_state !== "valid" ||
|
|
@@ -678,34 +701,47 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
678
701
|
!session.device_token) {
|
|
679
702
|
return;
|
|
680
703
|
}
|
|
681
|
-
const
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
704
|
+
const pending = (await readPendingInstallEventEntries(paths)).slice(0, 20);
|
|
705
|
+
const failures = [];
|
|
706
|
+
for (let offset = 0; offset < pending.length; offset += 5) {
|
|
707
|
+
await Promise.all(pending.slice(offset, offset + 5).map(async (entry) => {
|
|
708
|
+
const controller = new AbortController();
|
|
709
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
710
|
+
try {
|
|
711
|
+
const response = await options.io.fetch(`${entry.dashboard_url}/api/ambient/install-events`, {
|
|
712
|
+
method: "POST",
|
|
713
|
+
headers: {
|
|
714
|
+
"Content-Type": "application/json",
|
|
715
|
+
Authorization: `Bearer ${session.device_token}`,
|
|
716
|
+
},
|
|
717
|
+
body: JSON.stringify({
|
|
718
|
+
cli_version: entry.cli_version,
|
|
719
|
+
command: entry.command,
|
|
720
|
+
os_platform: entry.os_platform,
|
|
721
|
+
events: entry.events,
|
|
722
|
+
}),
|
|
723
|
+
signal: controller.signal,
|
|
724
|
+
});
|
|
725
|
+
if (!response.ok) {
|
|
726
|
+
throw new Error(`http_${response.status}`);
|
|
727
|
+
}
|
|
728
|
+
await removeInstallEventEntry(paths, entry.outbox_id);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
const failureReason = classifyInstallTelemetryError(error);
|
|
732
|
+
failures.push(failureReason);
|
|
733
|
+
await recordInstallEventAttemptFailure(paths, entry, {
|
|
734
|
+
attemptedAt: new Date().toISOString(),
|
|
735
|
+
failureReason,
|
|
736
|
+
}).catch(() => undefined);
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
clearTimeout(timeout);
|
|
740
|
+
}
|
|
741
|
+
}));
|
|
706
742
|
}
|
|
707
|
-
|
|
708
|
-
|
|
743
|
+
if (options.json && failures.length > 0) {
|
|
744
|
+
writeLine(options.io.stderr, `Install event telemetry queued for retry: ${[...new Set(failures)].join(",")}`);
|
|
709
745
|
}
|
|
710
746
|
}
|
|
711
747
|
function classifyInstallTelemetryError(error) {
|
|
@@ -1742,6 +1778,54 @@ async function runStart(command, io) {
|
|
|
1742
1778
|
return 0;
|
|
1743
1779
|
}
|
|
1744
1780
|
async function runSync(command, io) {
|
|
1781
|
+
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
1782
|
+
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
1783
|
+
const dashboardUrl = command.dashboardUrl ?? config?.dashboard_url ?? DEFAULT_DASHBOARD_URL;
|
|
1784
|
+
await reportInstallEventsBestEffort({
|
|
1785
|
+
homeDir: command.homeDir,
|
|
1786
|
+
dashboardUrl,
|
|
1787
|
+
command: "sync",
|
|
1788
|
+
events: [{ step: "sync_started", status: "ok" }],
|
|
1789
|
+
json: command.json,
|
|
1790
|
+
io,
|
|
1791
|
+
});
|
|
1792
|
+
try {
|
|
1793
|
+
const code = await runSyncWithHealthReceipt(command, io);
|
|
1794
|
+
await reportInstallEventsBestEffort({
|
|
1795
|
+
homeDir: command.homeDir,
|
|
1796
|
+
dashboardUrl,
|
|
1797
|
+
command: "sync",
|
|
1798
|
+
events: [
|
|
1799
|
+
{
|
|
1800
|
+
step: "sync_complete",
|
|
1801
|
+
status: code === 0 ? "ok" : "fail",
|
|
1802
|
+
...(code === 0 ? {} : { error_code: "sync_failed" }),
|
|
1803
|
+
},
|
|
1804
|
+
],
|
|
1805
|
+
json: command.json,
|
|
1806
|
+
io,
|
|
1807
|
+
});
|
|
1808
|
+
return code;
|
|
1809
|
+
}
|
|
1810
|
+
catch (error) {
|
|
1811
|
+
await reportInstallEventsBestEffort({
|
|
1812
|
+
homeDir: command.homeDir,
|
|
1813
|
+
dashboardUrl,
|
|
1814
|
+
command: "sync",
|
|
1815
|
+
events: [
|
|
1816
|
+
{
|
|
1817
|
+
step: "sync_complete",
|
|
1818
|
+
status: "fail",
|
|
1819
|
+
error_code: classifySyncHealthError(error),
|
|
1820
|
+
},
|
|
1821
|
+
],
|
|
1822
|
+
json: command.json,
|
|
1823
|
+
io,
|
|
1824
|
+
});
|
|
1825
|
+
throw error;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
async function runSyncWithHealthReceipt(command, io) {
|
|
1745
1829
|
const backfillLock = await inspectBackfillLock(getCollectorRuntimePaths(command.homeDir));
|
|
1746
1830
|
if (backfillLock.held) {
|
|
1747
1831
|
if (command.json) {
|
|
@@ -1775,6 +1859,19 @@ async function runSync(command, io) {
|
|
|
1775
1859
|
await lock.handle.release();
|
|
1776
1860
|
}
|
|
1777
1861
|
}
|
|
1862
|
+
function classifySyncHealthError(error) {
|
|
1863
|
+
const message = errorMessage(error);
|
|
1864
|
+
if (/auth|token|session|unauthorized|forbidden|401|403/iu.test(message)) {
|
|
1865
|
+
return "auth_failed";
|
|
1866
|
+
}
|
|
1867
|
+
if (/fetch|network|enotfound|econnrefused|timeout/iu.test(message)) {
|
|
1868
|
+
return "network_failed";
|
|
1869
|
+
}
|
|
1870
|
+
if (/collection.root|workspace|repo|worktree/iu.test(message)) {
|
|
1871
|
+
return "collection_root_failed";
|
|
1872
|
+
}
|
|
1873
|
+
return "sync_failed";
|
|
1874
|
+
}
|
|
1778
1875
|
async function runSyncLocked(command, io) {
|
|
1779
1876
|
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos }, io);
|
|
1780
1877
|
const run = await runAttributedWorktreeSync({
|
|
@@ -2015,6 +2112,8 @@ async function runStatus(command, io) {
|
|
|
2015
2112
|
writeLine(io.stdout, `last_upload_success: ${status.last_upload_success_at ?? "never"}`);
|
|
2016
2113
|
writeLine(io.stdout, `last_upload_failure: ${status.last_upload_failure_reason ?? "none"}`);
|
|
2017
2114
|
writeLine(io.stdout, `pending_uploads: ${status.pending_upload_count}`);
|
|
2115
|
+
writeLine(io.stdout, `pending_health_receipts: ${status.pending_health_receipt_count}`);
|
|
2116
|
+
writeLine(io.stdout, `last_health_receipt_failure: ${status.last_health_receipt_failure_reason ?? "none"}`);
|
|
2018
2117
|
writeLine(io.stdout, `backfill: ${backfillCursorLine(backfillCursor)}`);
|
|
2019
2118
|
for (const detail of status.details)
|
|
2020
2119
|
writeLine(io.stdout, `- ${detail}`);
|
package/dist/local-state.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { resolveRepoWorktreeIdentity, stableWorktreeFingerprint, stableWorktreeRoot, } from "./repo-identity.js";
|
|
8
8
|
import { normalizeCollectionRoots } from "./root-normalization.js";
|
|
9
9
|
import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
|
|
10
|
+
import { summarizeInstallEventOutbox } from "./spool/install-event-outbox.js";
|
|
10
11
|
const localCollectorPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
12
|
export const LOCAL_COLLECTOR_VERSION = typeof localCollectorPackage.version === "string"
|
|
12
13
|
? localCollectorPackage.version
|
|
@@ -237,7 +238,10 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
237
238
|
const identity = await resolveIdentityOrFallback(repoRoot, options.branch);
|
|
238
239
|
const context = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => null);
|
|
239
240
|
const branch = options.branch ?? identity.branch;
|
|
240
|
-
const uploadSpool = await
|
|
241
|
+
const [uploadSpool, healthOutbox] = await Promise.all([
|
|
242
|
+
summarizeLocalUploadSpool(paths),
|
|
243
|
+
summarizeInstallEventOutbox(paths),
|
|
244
|
+
]);
|
|
241
245
|
const freshness = classifyCollectorFreshness(context, uploadSpool.last_upload_success_at, now);
|
|
242
246
|
const uploadState = !config
|
|
243
247
|
? "not_installed"
|
|
@@ -268,6 +272,9 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
268
272
|
if (uploadSpool.pending_upload_count > 0) {
|
|
269
273
|
details.push(`Upload retry pending: ${uploadSpool.pending_upload_count} safe metadata record(s) spooled. Run \`${uploadSpool.retry_command ?? "cockpit sync"}\` to retry.`);
|
|
270
274
|
}
|
|
275
|
+
if (healthOutbox.pending_count > 0) {
|
|
276
|
+
details.push(`Collector health retry pending: ${healthOutbox.pending_count} sanitized receipt(s) queued since ${healthOutbox.oldest_created_at ?? "unknown"}.`);
|
|
277
|
+
}
|
|
271
278
|
return {
|
|
272
279
|
installed: Boolean(config),
|
|
273
280
|
config_file: paths.config_file,
|
|
@@ -293,6 +300,9 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
293
300
|
last_upload_failure_reason: uploadSpool.last_upload_failure_reason,
|
|
294
301
|
pending_upload_count: uploadSpool.pending_upload_count,
|
|
295
302
|
upload_retry_command: uploadSpool.retry_command,
|
|
303
|
+
pending_health_receipt_count: healthOutbox.pending_count,
|
|
304
|
+
oldest_pending_health_receipt_at: healthOutbox.oldest_created_at,
|
|
305
|
+
last_health_receipt_failure_reason: healthOutbox.last_failure_reason,
|
|
296
306
|
details,
|
|
297
307
|
};
|
|
298
308
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const OUTBOX_DIRECTORY = "install-events";
|
|
5
|
+
const MAX_PENDING_ENTRIES = 100;
|
|
6
|
+
const MAX_EVENTS_PER_ENTRY = 40;
|
|
7
|
+
export async function enqueueInstallEventEntry(paths, options) {
|
|
8
|
+
if (options.events.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
const createdAt = (options.now ?? new Date()).toISOString();
|
|
11
|
+
const entry = {
|
|
12
|
+
schema_version: "cockpit-install-event-outbox.v1",
|
|
13
|
+
outbox_id: `install-event-${crypto.randomUUID()}`,
|
|
14
|
+
created_at: createdAt,
|
|
15
|
+
last_attempt_at: null,
|
|
16
|
+
retry_count: 0,
|
|
17
|
+
last_failure_reason: null,
|
|
18
|
+
dashboard_url: options.dashboardUrl,
|
|
19
|
+
cli_version: options.cliVersion,
|
|
20
|
+
command: options.command,
|
|
21
|
+
os_platform: options.osPlatform,
|
|
22
|
+
events: options.events.slice(0, MAX_EVENTS_PER_ENTRY).map((event) => ({
|
|
23
|
+
step: event.step,
|
|
24
|
+
status: event.status,
|
|
25
|
+
...(event.error_code ? { error_code: event.error_code } : {}),
|
|
26
|
+
at: event.at ?? createdAt,
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
await writeEntry(paths, entry);
|
|
30
|
+
await pruneInstallEventOutbox(paths);
|
|
31
|
+
return entry;
|
|
32
|
+
}
|
|
33
|
+
export async function readPendingInstallEventEntries(paths) {
|
|
34
|
+
const directory = installEventOutboxDirectory(paths);
|
|
35
|
+
const names = await fs.readdir(directory).catch(() => []);
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (const name of names.filter((candidate) => candidate.endsWith(".json"))) {
|
|
38
|
+
const filePath = path.join(directory, name);
|
|
39
|
+
try {
|
|
40
|
+
const parsed = parseEntry(JSON.parse(await fs.readFile(filePath, "utf8")));
|
|
41
|
+
if (!parsed) {
|
|
42
|
+
await fs.rm(filePath, { force: true });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
entries.push(parsed);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
await fs.rm(filePath, { force: true }).catch(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return entries.sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at) ||
|
|
52
|
+
left.outbox_id.localeCompare(right.outbox_id));
|
|
53
|
+
}
|
|
54
|
+
export async function summarizeInstallEventOutbox(paths) {
|
|
55
|
+
const entries = await readPendingInstallEventEntries(paths);
|
|
56
|
+
const attempted = entries
|
|
57
|
+
.filter((entry) => entry.last_attempt_at)
|
|
58
|
+
.sort((left, right) => Date.parse(right.last_attempt_at) - Date.parse(left.last_attempt_at))[0];
|
|
59
|
+
return {
|
|
60
|
+
pending_count: entries.length,
|
|
61
|
+
oldest_created_at: entries[0]?.created_at ?? null,
|
|
62
|
+
last_attempt_at: attempted?.last_attempt_at ?? null,
|
|
63
|
+
last_failure_reason: attempted?.last_failure_reason ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function recordInstallEventAttemptFailure(paths, entry, options) {
|
|
67
|
+
await writeEntry(paths, {
|
|
68
|
+
...entry,
|
|
69
|
+
last_attempt_at: options.attemptedAt,
|
|
70
|
+
retry_count: entry.retry_count + 1,
|
|
71
|
+
last_failure_reason: options.failureReason,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export async function removeInstallEventEntry(paths, outboxId) {
|
|
75
|
+
await fs.rm(entryPath(paths, outboxId), { force: true });
|
|
76
|
+
}
|
|
77
|
+
export function installEventOutboxDirectory(paths) {
|
|
78
|
+
return path.join(paths.spool_dir, OUTBOX_DIRECTORY);
|
|
79
|
+
}
|
|
80
|
+
async function pruneInstallEventOutbox(paths) {
|
|
81
|
+
const entries = await readPendingInstallEventEntries(paths);
|
|
82
|
+
const excess = entries.slice(0, Math.max(0, entries.length - MAX_PENDING_ENTRIES));
|
|
83
|
+
await Promise.all(excess.map((entry) => removeInstallEventEntry(paths, entry.outbox_id)));
|
|
84
|
+
}
|
|
85
|
+
async function writeEntry(paths, entry) {
|
|
86
|
+
const directory = installEventOutboxDirectory(paths);
|
|
87
|
+
const filePath = entryPath(paths, entry.outbox_id);
|
|
88
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
89
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
90
|
+
if (process.platform !== "win32") {
|
|
91
|
+
await fs.chmod(directory, 0o700).catch(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await fs.writeFile(tempPath, `${JSON.stringify(entry, null, 2)}\n`, {
|
|
95
|
+
mode: 0o600,
|
|
96
|
+
flag: "wx",
|
|
97
|
+
});
|
|
98
|
+
await fs.rename(tempPath, filePath);
|
|
99
|
+
if (process.platform !== "win32") {
|
|
100
|
+
await fs.chmod(filePath, 0o600).catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function entryPath(paths, outboxId) {
|
|
108
|
+
return path.join(installEventOutboxDirectory(paths), `${safeOutboxId(outboxId)}.json`);
|
|
109
|
+
}
|
|
110
|
+
function safeOutboxId(value) {
|
|
111
|
+
return value.replace(/[^a-z0-9_-]/giu, "").slice(0, 120);
|
|
112
|
+
}
|
|
113
|
+
function parseEntry(value) {
|
|
114
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
115
|
+
return null;
|
|
116
|
+
const record = value;
|
|
117
|
+
const command = installEventCommand(record["command"]);
|
|
118
|
+
const events = Array.isArray(record["events"])
|
|
119
|
+
? record["events"].map(parseEvent).filter(isPresent)
|
|
120
|
+
: [];
|
|
121
|
+
if (record["schema_version"] !== "cockpit-install-event-outbox.v1" ||
|
|
122
|
+
!stringValue(record["outbox_id"]) ||
|
|
123
|
+
!stringValue(record["created_at"]) ||
|
|
124
|
+
!stringValue(record["dashboard_url"]) ||
|
|
125
|
+
!stringValue(record["cli_version"]) ||
|
|
126
|
+
!stringValue(record["os_platform"]) ||
|
|
127
|
+
!command ||
|
|
128
|
+
events.length === 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
schema_version: "cockpit-install-event-outbox.v1",
|
|
133
|
+
outbox_id: stringValue(record["outbox_id"]),
|
|
134
|
+
created_at: stringValue(record["created_at"]),
|
|
135
|
+
last_attempt_at: stringValue(record["last_attempt_at"]),
|
|
136
|
+
retry_count: finiteNumber(record["retry_count"]),
|
|
137
|
+
last_failure_reason: stringValue(record["last_failure_reason"]),
|
|
138
|
+
dashboard_url: stringValue(record["dashboard_url"]),
|
|
139
|
+
cli_version: stringValue(record["cli_version"]),
|
|
140
|
+
command,
|
|
141
|
+
os_platform: stringValue(record["os_platform"]),
|
|
142
|
+
events: events.slice(0, MAX_EVENTS_PER_ENTRY),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseEvent(value) {
|
|
146
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
147
|
+
return null;
|
|
148
|
+
const record = value;
|
|
149
|
+
const step = stringValue(record["step"]);
|
|
150
|
+
const status = installEventStatus(record["status"]);
|
|
151
|
+
const at = stringValue(record["at"]);
|
|
152
|
+
if (!step || !status || !at)
|
|
153
|
+
return null;
|
|
154
|
+
return {
|
|
155
|
+
step,
|
|
156
|
+
status,
|
|
157
|
+
...(stringValue(record["error_code"])
|
|
158
|
+
? { error_code: stringValue(record["error_code"]) }
|
|
159
|
+
: {}),
|
|
160
|
+
at,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function installEventCommand(value) {
|
|
164
|
+
return [
|
|
165
|
+
"onboard",
|
|
166
|
+
"update",
|
|
167
|
+
"install",
|
|
168
|
+
"login",
|
|
169
|
+
"sync",
|
|
170
|
+
"backfill",
|
|
171
|
+
"doctor",
|
|
172
|
+
].includes(String(value))
|
|
173
|
+
? value
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
176
|
+
function installEventStatus(value) {
|
|
177
|
+
return value === "ok" || value === "fail" || value === "skipped"
|
|
178
|
+
? value
|
|
179
|
+
: null;
|
|
180
|
+
}
|
|
181
|
+
function stringValue(value) {
|
|
182
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
183
|
+
}
|
|
184
|
+
function finiteNumber(value) {
|
|
185
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
186
|
+
? Math.floor(value)
|
|
187
|
+
: 0;
|
|
188
|
+
}
|
|
189
|
+
function isPresent(value) {
|
|
190
|
+
return value !== null;
|
|
191
|
+
}
|