@indigoai-us/hq-cloud 6.11.13 → 6.11.15
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/.github/workflows/ci.yml +18 -26
- package/dist/bin/sync-runner-telemetry.d.ts +1 -0
- package/dist/bin/sync-runner-telemetry.d.ts.map +1 -1
- package/dist/bin/sync-runner-telemetry.js +8 -1
- package/dist/bin/sync-runner-telemetry.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +9 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +115 -9
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +15 -1
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +26 -2
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +16 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/qmd-reindex.d.ts +7 -0
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +12 -1
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/sync-progress.d.ts +37 -0
- package/dist/sync-progress.d.ts.map +1 -0
- package/dist/sync-progress.js +104 -0
- package/dist/sync-progress.js.map +1 -0
- package/dist/sync-progress.test.d.ts +2 -0
- package/dist/sync-progress.test.d.ts.map +1 -0
- package/dist/sync-progress.test.js +70 -0
- package/dist/sync-progress.test.js.map +1 -0
- package/package.json +3 -1
- package/src/bin/sync-runner-telemetry.ts +8 -1
- package/src/bin/sync-runner.ts +131 -7
- package/src/cli/share.ts +17 -1
- package/src/cli/sync.test.ts +17 -0
- package/src/cli/sync.ts +35 -2
- package/src/index.ts +9 -0
- package/src/qmd-reindex.ts +18 -1
- package/src/sync-progress.test.ts +81 -0
- package/src/sync-progress.ts +133 -0
package/dist/qmd-reindex.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qmd-reindex.js","sourceRoot":"","sources":["../src/qmd-reindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAO1C,MAAM,WAAW,GAAY,CAAC,IAAI,EAAE,EAAE;IACpC,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;QACjC,QAAQ,EAAE,MAAM;QAChB,qEAAqE;QACrE,wCAAwC;QACxC,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AAC1D,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"qmd-reindex.js","sourceRoot":"","sources":["../src/qmd-reindex.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SyncProgressEvent } from "./cli/sync.js";
|
|
2
|
+
/** Absolute path of the shared progress snapshot. Exported for the watcher + tests. */
|
|
3
|
+
export declare function syncProgressPath(): string;
|
|
4
|
+
export interface SyncProgressSnapshot {
|
|
5
|
+
schema: 1;
|
|
6
|
+
/** PID of the writing sync — lets a watcher detect a dead/stale holder. */
|
|
7
|
+
pid: number;
|
|
8
|
+
/** Company slug being synced, or null for the personal vault. */
|
|
9
|
+
company: string | null;
|
|
10
|
+
/** Which leg is running. */
|
|
11
|
+
phase: "pull" | "push";
|
|
12
|
+
filesTotal: number;
|
|
13
|
+
filesDone: number;
|
|
14
|
+
conflicts: number;
|
|
15
|
+
/** Path of the file currently transferring, or null before the first one. */
|
|
16
|
+
currentFile: string | null;
|
|
17
|
+
startedAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
status: "syncing";
|
|
20
|
+
}
|
|
21
|
+
export interface SyncProgressRecorderOptions {
|
|
22
|
+
company?: string | null;
|
|
23
|
+
phase: "pull" | "push";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build an event sink that accumulates a sync's progress and atomic-writes the
|
|
27
|
+
* shared snapshot on each meaningful event. Wrap the engine's `onEvent` with
|
|
28
|
+
* it: `const emit = (e) => { baseEmit(e); record(e); }`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createSyncProgressRecorder(opts: SyncProgressRecorderOptions): (event: SyncProgressEvent) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Read the current snapshot, or null if absent/unreadable. Consumers should
|
|
33
|
+
* additionally treat a snapshot whose `updatedAt` is older than a few seconds
|
|
34
|
+
* (or whose `pid` is dead) as idle.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readSyncProgress(): SyncProgressSnapshot | null;
|
|
37
|
+
//# sourceMappingURL=sync-progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-progress.d.ts","sourceRoot":"","sources":["../src/sync-progress.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAOvD,uFAAuF;AACvF,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,CAAC,CAAC;IACV,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,2BAA2B,GAChC,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAwDpC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,oBAAoB,GAAG,IAAI,CAQ9D"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process sync progress.
|
|
3
|
+
*
|
|
4
|
+
* Every sync path — the menubar watch daemon (auto-sync), "Sync Now", and a
|
|
5
|
+
* CLI `hq sync` — runs through the same engine (`sync()` pull + `share()`
|
|
6
|
+
* push). Each writes one shared status snapshot here so a watcher (the menubar)
|
|
7
|
+
* can show LIVE progress for ANY sync, not just the one it spawned over stdout.
|
|
8
|
+
*
|
|
9
|
+
* Because syncs are serialized (the op-lock; the watch daemon coordinates with
|
|
10
|
+
* one-shots), there is at most one active sync, so this single file is
|
|
11
|
+
* unambiguous. Writes are atomic (temp + rename) — a torn read at worst shows
|
|
12
|
+
* slightly stale progress. There is intentionally no explicit "done": the
|
|
13
|
+
* engine simply stops writing when a sync ends, so a consumer treats the file
|
|
14
|
+
* as idle once `updatedAt` is older than a few seconds. Progress reporting must
|
|
15
|
+
* never break a sync, so every fs error here is swallowed.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as os from "os";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
/** Mirror operation-lock's stateDir so the file lands next to the journal/lock. */
|
|
21
|
+
function stateDir() {
|
|
22
|
+
return process.env.HQ_STATE_DIR || path.join(os.homedir(), ".hq");
|
|
23
|
+
}
|
|
24
|
+
/** Absolute path of the shared progress snapshot. Exported for the watcher + tests. */
|
|
25
|
+
export function syncProgressPath() {
|
|
26
|
+
return path.join(stateDir(), "sync-progress.json");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build an event sink that accumulates a sync's progress and atomic-writes the
|
|
30
|
+
* shared snapshot on each meaningful event. Wrap the engine's `onEvent` with
|
|
31
|
+
* it: `const emit = (e) => { baseEmit(e); record(e); }`.
|
|
32
|
+
*/
|
|
33
|
+
export function createSyncProgressRecorder(opts) {
|
|
34
|
+
let filesTotal = 0;
|
|
35
|
+
let filesDone = 0;
|
|
36
|
+
let conflicts = 0;
|
|
37
|
+
let currentFile = null;
|
|
38
|
+
const startedAt = new Date().toISOString();
|
|
39
|
+
const file = syncProgressPath();
|
|
40
|
+
const flush = () => {
|
|
41
|
+
const snap = {
|
|
42
|
+
schema: 1,
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
company: opts.company ?? null,
|
|
45
|
+
phase: opts.phase,
|
|
46
|
+
filesTotal,
|
|
47
|
+
filesDone,
|
|
48
|
+
conflicts,
|
|
49
|
+
currentFile,
|
|
50
|
+
startedAt,
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
status: "syncing",
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
56
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
57
|
+
fs.writeFileSync(tmp, JSON.stringify(snap));
|
|
58
|
+
fs.renameSync(tmp, file);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Progress reporting is best-effort and must never break a sync.
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return (event) => {
|
|
65
|
+
switch (event.type) {
|
|
66
|
+
case "plan":
|
|
67
|
+
// Sum the work this leg will do. `plan` arrives once before any
|
|
68
|
+
// progress events for the leg.
|
|
69
|
+
filesTotal +=
|
|
70
|
+
event.filesToDownload + event.filesToUpload + event.filesToDelete;
|
|
71
|
+
flush();
|
|
72
|
+
break;
|
|
73
|
+
case "progress":
|
|
74
|
+
filesDone += 1;
|
|
75
|
+
currentFile = event.path;
|
|
76
|
+
flush();
|
|
77
|
+
break;
|
|
78
|
+
case "conflict":
|
|
79
|
+
conflicts += 1;
|
|
80
|
+
flush();
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
// error / new-files / reconciled / delete-refused etc. don't move the
|
|
84
|
+
// bar — skip the write to keep the file churn-free.
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read the current snapshot, or null if absent/unreadable. Consumers should
|
|
91
|
+
* additionally treat a snapshot whose `updatedAt` is older than a few seconds
|
|
92
|
+
* (or whose `pid` is dead) as idle.
|
|
93
|
+
*/
|
|
94
|
+
export function readSyncProgress() {
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(syncProgressPath(), "utf8");
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
return parsed && parsed.schema === 1 ? parsed : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=sync-progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-progress.js","sourceRoot":"","sources":["../src/sync-progress.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-progress.test.d.ts","sourceRoot":"","sources":["../src/sync-progress.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { createSyncProgressRecorder, readSyncProgress, } from "./sync-progress.js";
|
|
6
|
+
const planEvent = (download, upload, del) => ({
|
|
7
|
+
type: "plan",
|
|
8
|
+
filesToDownload: download,
|
|
9
|
+
bytesToDownload: 0,
|
|
10
|
+
filesToUpload: upload,
|
|
11
|
+
bytesToUpload: 0,
|
|
12
|
+
filesToSkip: 0,
|
|
13
|
+
filesToConflict: 0,
|
|
14
|
+
filesToDelete: del,
|
|
15
|
+
});
|
|
16
|
+
describe("sync-progress", () => {
|
|
17
|
+
let stateDir;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-progress-test-"));
|
|
20
|
+
process.env.HQ_STATE_DIR = stateDir;
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
delete process.env.HQ_STATE_DIR;
|
|
24
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
it("returns null when no sync has written a snapshot", () => {
|
|
27
|
+
expect(readSyncProgress()).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
it("accumulates plan totals, progress, and conflicts into the shared snapshot", () => {
|
|
30
|
+
const record = createSyncProgressRecorder({ company: "acme", phase: "pull" });
|
|
31
|
+
record(planEvent(3, 0, 1)); // 3 to download + 1 to delete = 4 total
|
|
32
|
+
record({ type: "progress", path: "a.md", bytes: 10 });
|
|
33
|
+
record({ type: "progress", path: "b.md", bytes: 10 });
|
|
34
|
+
record({
|
|
35
|
+
type: "conflict",
|
|
36
|
+
path: "c.md",
|
|
37
|
+
direction: "pull",
|
|
38
|
+
resolution: "keep",
|
|
39
|
+
});
|
|
40
|
+
const snap = readSyncProgress();
|
|
41
|
+
expect(snap).not.toBeNull();
|
|
42
|
+
expect(snap.company).toBe("acme");
|
|
43
|
+
expect(snap.phase).toBe("pull");
|
|
44
|
+
expect(snap.filesTotal).toBe(4);
|
|
45
|
+
expect(snap.filesDone).toBe(2);
|
|
46
|
+
expect(snap.conflicts).toBe(1);
|
|
47
|
+
expect(snap.currentFile).toBe("b.md");
|
|
48
|
+
expect(snap.status).toBe("syncing");
|
|
49
|
+
expect(snap.pid).toBe(process.pid);
|
|
50
|
+
expect(typeof snap.updatedAt).toBe("string");
|
|
51
|
+
});
|
|
52
|
+
it("records the push leg + personal scope (company null)", () => {
|
|
53
|
+
const record = createSyncProgressRecorder({ company: null, phase: "push" });
|
|
54
|
+
record(planEvent(0, 5, 0));
|
|
55
|
+
const snap = readSyncProgress();
|
|
56
|
+
expect(snap.phase).toBe("push");
|
|
57
|
+
expect(snap.company).toBeNull();
|
|
58
|
+
expect(snap.filesTotal).toBe(5);
|
|
59
|
+
});
|
|
60
|
+
it("ignores non-progress events (no churn from new-files/reconciled)", () => {
|
|
61
|
+
const record = createSyncProgressRecorder({ company: "acme", phase: "pull" });
|
|
62
|
+
record(planEvent(1, 0, 0));
|
|
63
|
+
record({ type: "new-files", files: [] });
|
|
64
|
+
record({ type: "reconciled", path: "x.md", direction: "pull" });
|
|
65
|
+
const snap = readSyncProgress();
|
|
66
|
+
expect(snap.filesDone).toBe(0);
|
|
67
|
+
expect(snap.filesTotal).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=sync-progress.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-progress.test.js","sourceRoot":"","sources":["../src/sync-progress.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,MAAc,EAAE,GAAW,EAAqB,EAAE,CACrF,CAAC;IACC,IAAI,EAAE,MAAM;IACZ,eAAe,EAAE,QAAQ;IACzB,eAAe,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM;IACrB,aAAa,EAAE,CAAC;IAChB,WAAW,EAAE,CAAC;IACd,eAAe,EAAE,CAAC;IAClB,aAAa,EAAE,GAAG;CACnB,CAAsB,CAAC;AAE1B,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAI,QAAgB,CAAC;IACrB,UAAU,CAAC,GAAG,EAAE;QACd,QAAQ,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,QAAQ,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QAChC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACnF,MAAM,MAAM,GAAG,0BAA0B,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9E,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,wCAAwC;QACpE,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAuB,CAAC,CAAC;QAC3E,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAuB,CAAC,CAAC;QAC3E,MAAM,CAAC;YACL,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,MAAM;YACZ,SAAS,EAAE,MAAM;YACjB,UAAU,EAAE,MAAM;SACE,CAAC,CAAC;QAExB,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC5B,MAAM,CAAC,IAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,IAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,IAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,IAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,IAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,IAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,CAAC,IAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrC,MAAM,CAAC,IAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,CAAC,OAAO,IAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,MAAM,GAAG,0BAA0B,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;QAChC,MAAM,CAAC,IAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,IAAK,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,CAAC,IAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,MAAM,GAAG,0BAA0B,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9E,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAuB,CAAC,CAAC;QAC9D,MAAM,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAuB,CAAC,CAAC;QACrF,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;QAChC,MAAM,CAAC,IAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,IAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cloud",
|
|
3
|
-
"version": "6.11.
|
|
3
|
+
"version": "6.11.15",
|
|
4
4
|
"description": "HQ by Indigo cloud sync engine — bidirectional S3 sync for mobile access",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
14
|
"test": "vitest run src/",
|
|
15
|
+
"coverage": "vitest run src/ --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
|
|
15
16
|
"test:e2e": "vitest run test/",
|
|
16
17
|
"test:watch": "vitest",
|
|
17
18
|
"clean": "rm -rf dist"
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"@types/js-yaml": "^4.0.9",
|
|
34
35
|
"@types/node": "^22.0.0",
|
|
35
36
|
"aws-sdk-client-mock": "^4.1.0",
|
|
37
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
36
38
|
"typescript": "^5.7.0",
|
|
37
39
|
"vitest": "^3.0.0"
|
|
38
40
|
},
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export async function emitTelemetry(options: {
|
|
2
2
|
collectTelemetry?: () => Promise<void>;
|
|
3
3
|
defaultCollectTelemetry: () => Promise<void>;
|
|
4
|
+
onError?: (err: unknown) => void;
|
|
4
5
|
}): Promise<void> {
|
|
5
6
|
const telemetryFn =
|
|
6
7
|
options.collectTelemetry ?? options.defaultCollectTelemetry;
|
|
7
|
-
await telemetryFn().catch(() =>
|
|
8
|
+
await telemetryFn().catch((err) => {
|
|
9
|
+
try {
|
|
10
|
+
options.onError?.(err);
|
|
11
|
+
} catch {
|
|
12
|
+
/* diagnostics must never affect sync */
|
|
13
|
+
}
|
|
14
|
+
});
|
|
8
15
|
}
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -107,6 +107,7 @@ import { collectAndSendSkillTelemetry } from "../skill-telemetry.js";
|
|
|
107
107
|
import { reindexAfterSync } from "../qmd-reindex.js";
|
|
108
108
|
import { pruneConflictIndex } from "../lib/conflict-index.js";
|
|
109
109
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
110
|
+
import { describeError } from "../lib/describe-error.js";
|
|
110
111
|
import type { Clock, TreeChangeBatch } from "../watcher.js";
|
|
111
112
|
import type { PushReceiver, SyncEngineFn } from "../sync/push-receiver.js";
|
|
112
113
|
import {
|
|
@@ -404,6 +405,53 @@ export interface VaultClientSurface {
|
|
|
404
405
|
listMyExplicitGrants?: (companyUid: string) => Promise<ExplicitGrant[]>;
|
|
405
406
|
}
|
|
406
407
|
|
|
408
|
+
interface RunnerDiagnostic {
|
|
409
|
+
component: string;
|
|
410
|
+
event: string;
|
|
411
|
+
message: string;
|
|
412
|
+
err?: unknown;
|
|
413
|
+
context?: Record<string, unknown>;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
type RunnerDiagnosticReporter = (diagnostic: RunnerDiagnostic) => void;
|
|
417
|
+
|
|
418
|
+
function errorDetails(err: unknown): Record<string, unknown> {
|
|
419
|
+
const details: Record<string, unknown> = { message: describeError(err) };
|
|
420
|
+
if (err instanceof Error) {
|
|
421
|
+
if (err.name) details.name = err.name;
|
|
422
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
423
|
+
if (code) details.code = code;
|
|
424
|
+
}
|
|
425
|
+
return details;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function createRunnerDiagnosticReporter(
|
|
429
|
+
stderr: { write: (chunk: string) => boolean | void },
|
|
430
|
+
): RunnerDiagnosticReporter {
|
|
431
|
+
return (diagnostic) => {
|
|
432
|
+
try {
|
|
433
|
+
const payload: Record<string, unknown> = {
|
|
434
|
+
type: "error",
|
|
435
|
+
diagnostic: true,
|
|
436
|
+
component: diagnostic.component,
|
|
437
|
+
event: diagnostic.event,
|
|
438
|
+
path: `(${diagnostic.component})`,
|
|
439
|
+
message:
|
|
440
|
+
diagnostic.err === undefined
|
|
441
|
+
? diagnostic.message
|
|
442
|
+
: `${diagnostic.message}: ${describeError(diagnostic.err)}`,
|
|
443
|
+
...(diagnostic.context ?? {}),
|
|
444
|
+
};
|
|
445
|
+
if (diagnostic.err !== undefined) {
|
|
446
|
+
payload.err = errorDetails(diagnostic.err);
|
|
447
|
+
}
|
|
448
|
+
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
449
|
+
} catch {
|
|
450
|
+
/* diagnostics must never affect runner control flow */
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
407
455
|
// `resolvePullScope`, `readPinnedPrefixes`, and the `PullScope` type now live
|
|
408
456
|
// in `../sync/pull-scope.ts` so the menubar runner and `hq sync pull|now`
|
|
409
457
|
// (hq-cli) share ONE scope resolver — the drift between them was the root
|
|
@@ -769,6 +817,7 @@ export async function defaultCollectTelemetry(
|
|
|
769
817
|
client: VaultClientSurface,
|
|
770
818
|
clientIsStub: boolean,
|
|
771
819
|
hqRoot: string,
|
|
820
|
+
reportDiagnostic: RunnerDiagnosticReporter = () => undefined,
|
|
772
821
|
): Promise<void> {
|
|
773
822
|
if (clientIsStub) return;
|
|
774
823
|
|
|
@@ -797,7 +846,14 @@ export async function defaultCollectTelemetry(
|
|
|
797
846
|
try {
|
|
798
847
|
const persons = await client.entity.listByType("person");
|
|
799
848
|
if (pickCanonicalPersonEntity(persons) === null) return;
|
|
800
|
-
} catch {
|
|
849
|
+
} catch (err) {
|
|
850
|
+
reportDiagnostic({
|
|
851
|
+
component: "telemetry",
|
|
852
|
+
event: "runner.telemetry.person_probe_failed",
|
|
853
|
+
message: "telemetry person probe failed; continuing with telemetry",
|
|
854
|
+
err,
|
|
855
|
+
context: { hqRoot },
|
|
856
|
+
});
|
|
801
857
|
// Probe failed — preserve prior behavior and let telemetry run.
|
|
802
858
|
}
|
|
803
859
|
|
|
@@ -820,7 +876,14 @@ export async function defaultCollectTelemetry(
|
|
|
820
876
|
try {
|
|
821
877
|
machineId = getOrCreateMachineId(hqRoot);
|
|
822
878
|
installerVersion = process.env.HQ_INSTALLER_VERSION ?? "hq-cloud";
|
|
823
|
-
} catch {
|
|
879
|
+
} catch (err) {
|
|
880
|
+
reportDiagnostic({
|
|
881
|
+
component: "telemetry",
|
|
882
|
+
event: "runner.telemetry.identity_failed",
|
|
883
|
+
message: "telemetry identity resolution failed; skipping telemetry",
|
|
884
|
+
err,
|
|
885
|
+
context: { hqRoot },
|
|
886
|
+
});
|
|
824
887
|
return;
|
|
825
888
|
}
|
|
826
889
|
|
|
@@ -836,8 +899,22 @@ export async function defaultCollectTelemetry(
|
|
|
836
899
|
// manifest.yaml and stamp companyUid (US-002). Unmatched cwds stay
|
|
837
900
|
// unattributed (field omitted).
|
|
838
901
|
hqRoot,
|
|
902
|
+
log: (message) =>
|
|
903
|
+
reportDiagnostic({
|
|
904
|
+
component: "telemetry",
|
|
905
|
+
event: "runner.telemetry.usage_nonfatal",
|
|
906
|
+
message,
|
|
907
|
+
context: { hqRoot },
|
|
908
|
+
}),
|
|
909
|
+
});
|
|
910
|
+
} catch (err) {
|
|
911
|
+
reportDiagnostic({
|
|
912
|
+
component: "telemetry",
|
|
913
|
+
event: "runner.telemetry.usage_failed",
|
|
914
|
+
message: "usage telemetry collector threw",
|
|
915
|
+
err,
|
|
916
|
+
context: { hqRoot },
|
|
839
917
|
});
|
|
840
|
-
} catch {
|
|
841
918
|
// Fire-and-forget; nothing escapes the boundary.
|
|
842
919
|
}
|
|
843
920
|
|
|
@@ -851,8 +928,22 @@ export async function defaultCollectTelemetry(
|
|
|
851
928
|
// Scope skill capture to the HQ project — only invocations whose cwd is
|
|
852
929
|
// the HQ root are emitted; skill usage in unrelated repos is excluded.
|
|
853
930
|
hqRoot,
|
|
931
|
+
log: (message) =>
|
|
932
|
+
reportDiagnostic({
|
|
933
|
+
component: "telemetry",
|
|
934
|
+
event: "runner.telemetry.skill_nonfatal",
|
|
935
|
+
message,
|
|
936
|
+
context: { hqRoot },
|
|
937
|
+
}),
|
|
938
|
+
});
|
|
939
|
+
} catch (err) {
|
|
940
|
+
reportDiagnostic({
|
|
941
|
+
component: "telemetry",
|
|
942
|
+
event: "runner.telemetry.skill_failed",
|
|
943
|
+
message: "skill telemetry collector threw",
|
|
944
|
+
err,
|
|
945
|
+
context: { hqRoot },
|
|
854
946
|
});
|
|
855
|
-
} catch {
|
|
856
947
|
// Fire-and-forget; nothing escapes the boundary.
|
|
857
948
|
}
|
|
858
949
|
}
|
|
@@ -867,6 +958,7 @@ export async function runRunner(
|
|
|
867
958
|
): Promise<number> {
|
|
868
959
|
const stdout = deps.stdout ?? process.stdout;
|
|
869
960
|
const stderr = deps.stderr ?? process.stderr;
|
|
961
|
+
const reportDiagnostic = createRunnerDiagnosticReporter(stderr);
|
|
870
962
|
|
|
871
963
|
// ---- emit ---------------------------------------------------------------
|
|
872
964
|
// Error-class events go to stderr; everything else to stdout.
|
|
@@ -1068,7 +1160,16 @@ export async function runRunner(
|
|
|
1068
1160
|
client,
|
|
1069
1161
|
deps.createVaultClient !== undefined,
|
|
1070
1162
|
parsed.hqRoot,
|
|
1163
|
+
reportDiagnostic,
|
|
1071
1164
|
),
|
|
1165
|
+
onError: (err) =>
|
|
1166
|
+
reportDiagnostic({
|
|
1167
|
+
component: "telemetry",
|
|
1168
|
+
event: "runner.telemetry.collector_failed",
|
|
1169
|
+
message: "telemetry collector failed",
|
|
1170
|
+
err,
|
|
1171
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1172
|
+
}),
|
|
1072
1173
|
});
|
|
1073
1174
|
|
|
1074
1175
|
emit({
|
|
@@ -1095,8 +1196,24 @@ export async function runRunner(
|
|
|
1095
1196
|
process.env.HQ_QMD_REINDEX_ON_SYNC !== "0"
|
|
1096
1197
|
) {
|
|
1097
1198
|
try {
|
|
1098
|
-
reindexAfterSync(parsed.hqRoot
|
|
1099
|
-
|
|
1199
|
+
reindexAfterSync(parsed.hqRoot, {
|
|
1200
|
+
log: ({ event, message, err, context }) =>
|
|
1201
|
+
reportDiagnostic({
|
|
1202
|
+
component: "qmd-reindex",
|
|
1203
|
+
event,
|
|
1204
|
+
message,
|
|
1205
|
+
err,
|
|
1206
|
+
context: { hqRoot: parsed.hqRoot, ...(context ?? {}) },
|
|
1207
|
+
}),
|
|
1208
|
+
});
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
reportDiagnostic({
|
|
1211
|
+
component: "qmd-reindex",
|
|
1212
|
+
event: "runner.qmd_reindex.failed",
|
|
1213
|
+
message: "post-sync qmd reindex threw",
|
|
1214
|
+
err,
|
|
1215
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1216
|
+
});
|
|
1100
1217
|
// Defensive: reindexAfterSync already swallows internally.
|
|
1101
1218
|
}
|
|
1102
1219
|
}
|
|
@@ -1113,7 +1230,14 @@ export async function runRunner(
|
|
|
1113
1230
|
if (process.env.HQ_CONFLICT_PRUNE_ON_SYNC !== "0") {
|
|
1114
1231
|
try {
|
|
1115
1232
|
pruneConflictIndex(parsed.hqRoot);
|
|
1116
|
-
} catch {
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
reportDiagnostic({
|
|
1235
|
+
component: "conflict-index",
|
|
1236
|
+
event: "runner.conflict_index.prune_failed",
|
|
1237
|
+
message: "post-sync conflict index prune failed",
|
|
1238
|
+
err,
|
|
1239
|
+
context: { hqRoot: parsed.hqRoot },
|
|
1240
|
+
});
|
|
1117
1241
|
// Defensive: a prune failure must never affect the sync outcome.
|
|
1118
1242
|
}
|
|
1119
1243
|
}
|
package/src/cli/share.ts
CHANGED
|
@@ -10,6 +10,7 @@ import * as os from "os";
|
|
|
10
10
|
import * as path from "path";
|
|
11
11
|
import type { EntityContext, VaultServiceConfig, SyncJournal } from "../types.js";
|
|
12
12
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
13
|
+
import { createSyncProgressRecorder } from "../sync-progress.js";
|
|
13
14
|
import {
|
|
14
15
|
uploadFile,
|
|
15
16
|
uploadSymlink,
|
|
@@ -886,7 +887,7 @@ async function createPushRunContext(options: ShareOptions): Promise<PushRunConte
|
|
|
886
887
|
const { paths, company, message, onConflict, vaultConfig, entityContext, hqRoot, skipUnchanged, propagateDeletes } = options;
|
|
887
888
|
let propagateDeletePolicy: DeletePolicy =
|
|
888
889
|
options.propagateDeletePolicy ?? "owned-only";
|
|
889
|
-
const
|
|
890
|
+
const baseEmit = options.onEvent ?? defaultConsoleLogger;
|
|
890
891
|
|
|
891
892
|
if (vaultConfig && entityContext) {
|
|
892
893
|
throw new Error(
|
|
@@ -918,6 +919,21 @@ async function createPushRunContext(options: ShareOptions): Promise<PushRunConte
|
|
|
918
919
|
propagateDeletePolicy = "currency-gated";
|
|
919
920
|
}
|
|
920
921
|
|
|
922
|
+
// Mirror push progress into the shared cross-process snapshot (see sync.ts).
|
|
923
|
+
// Record the friendly company slug, or null for the personal vault so the
|
|
924
|
+
// menubar shows "Personal" — never the raw prs_/cmp_ UID.
|
|
925
|
+
const recordProgress = createSyncProgressRecorder({
|
|
926
|
+
company:
|
|
927
|
+
ctx.uid.startsWith("prs_") || options.personalMode === true
|
|
928
|
+
? null
|
|
929
|
+
: ctx.slug,
|
|
930
|
+
phase: "push",
|
|
931
|
+
});
|
|
932
|
+
const emit: ShareEmit = (event) => {
|
|
933
|
+
baseEmit(event);
|
|
934
|
+
recordProgress(event);
|
|
935
|
+
};
|
|
936
|
+
|
|
921
937
|
const syncRoot = options.personalMode === true
|
|
922
938
|
? hqRoot
|
|
923
939
|
: path.join(hqRoot, "companies", ctx.slug);
|
package/src/cli/sync.test.ts
CHANGED
|
@@ -46,6 +46,7 @@ vi.mock("./reindex.js", () => ({
|
|
|
46
46
|
import { sync, reportNewFilesToNotify } from "./sync.js";
|
|
47
47
|
import * as s3Module from "../s3.js";
|
|
48
48
|
import { reindex } from "./reindex.js";
|
|
49
|
+
import { readSyncProgress } from "../sync-progress.js";
|
|
49
50
|
|
|
50
51
|
const mockConfig: VaultServiceConfig = {
|
|
51
52
|
apiUrl: "https://vault-api.test",
|
|
@@ -144,6 +145,22 @@ describe("sync", () => {
|
|
|
144
145
|
expect(reindex).toHaveBeenCalledWith({ repoRoot: tmpDir, skipLock: true });
|
|
145
146
|
});
|
|
146
147
|
|
|
148
|
+
it("records the company slug, not the input UID, in the shared progress snapshot", async () => {
|
|
149
|
+
// sync() is called with the company UID, but the menubar popover must show
|
|
150
|
+
// the friendly slug — so the producer records ctx.slug, never the raw UID.
|
|
151
|
+
vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
|
|
152
|
+
{ key: "docs/x.md", size: 10, lastModified: new Date(), etag: '"x"' },
|
|
153
|
+
]);
|
|
154
|
+
await sync({
|
|
155
|
+
company: "cmp_01ABCDEF",
|
|
156
|
+
vaultConfig: mockConfig,
|
|
157
|
+
hqRoot: tmpDir,
|
|
158
|
+
});
|
|
159
|
+
const snap = readSyncProgress();
|
|
160
|
+
expect(snap).not.toBeNull();
|
|
161
|
+
expect(snap!.company).toBe("acme"); // ctx.slug, not "cmp_01ABCDEF"
|
|
162
|
+
});
|
|
163
|
+
|
|
147
164
|
it("F15: public sync entrypoint refuses an already-held operation lock", async () => {
|
|
148
165
|
process.env.HQ_OP_LOCK_TIMEOUT = "0";
|
|
149
166
|
const lockPath = lockPathFor(tmpDir);
|
package/src/cli/sync.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from "../types.js";
|
|
15
15
|
import type { SyncMode } from "../vault-client.js";
|
|
16
16
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
17
|
+
import { createSyncProgressRecorder } from "../sync-progress.js";
|
|
17
18
|
import {
|
|
18
19
|
downloadFile,
|
|
19
20
|
listRemoteFiles,
|
|
@@ -657,6 +658,23 @@ function logNotifyFailure(err: unknown): void {
|
|
|
657
658
|
}
|
|
658
659
|
}
|
|
659
660
|
|
|
661
|
+
/** Log a non-fatal post-sync maintenance failure without changing sync state. */
|
|
662
|
+
function logPostSyncMaintenanceFailure(
|
|
663
|
+
task: string,
|
|
664
|
+
hqRoot: string,
|
|
665
|
+
err: unknown,
|
|
666
|
+
): void {
|
|
667
|
+
try {
|
|
668
|
+
console.error(
|
|
669
|
+
`[hq-sync] ${task} failed (non-fatal; hqRoot=${hqRoot}): ${
|
|
670
|
+
err instanceof Error ? err.message : String(err)
|
|
671
|
+
}`,
|
|
672
|
+
);
|
|
673
|
+
} catch {
|
|
674
|
+
// swallow — logging must never break sync
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
660
678
|
/**
|
|
661
679
|
* Sync (pull) all allowed files from the entity vault.
|
|
662
680
|
*/
|
|
@@ -708,7 +726,7 @@ async function syncWithOperationLockHeld(
|
|
|
708
726
|
|
|
709
727
|
async function buildPullContext(options: SyncOptions): Promise<PullRunContext> {
|
|
710
728
|
const { company, vaultConfig, hqRoot } = options;
|
|
711
|
-
const
|
|
729
|
+
const baseEmit = options.onEvent ?? defaultConsoleLogger;
|
|
712
730
|
|
|
713
731
|
const companyRef = company ?? resolveActiveCompany(hqRoot);
|
|
714
732
|
if (!companyRef) {
|
|
@@ -726,6 +744,20 @@ async function buildPullContext(options: SyncOptions): Promise<PullRunContext> {
|
|
|
726
744
|
const journalSlug = options.journalSlug ?? ctx.slug;
|
|
727
745
|
const startedAt = new Date().toISOString();
|
|
728
746
|
|
|
747
|
+
// Mirror every progress event into the shared cross-process snapshot so the
|
|
748
|
+
// menubar shows live progress for THIS sync regardless of who launched it
|
|
749
|
+
// (auto-sync / Sync Now / CLI). Record the friendly company slug (e.g.
|
|
750
|
+
// "indigo") — or null for the personal vault so the menubar shows "Personal"
|
|
751
|
+
// — never the raw prs_/cmp_ UID. Best-effort; never affects the sync.
|
|
752
|
+
const recordProgress = createSyncProgressRecorder({
|
|
753
|
+
company: options.personalMode === true ? null : ctx.slug,
|
|
754
|
+
phase: "pull",
|
|
755
|
+
});
|
|
756
|
+
const emit: SyncEventEmitter = (event) => {
|
|
757
|
+
baseEmit(event);
|
|
758
|
+
recordProgress(event);
|
|
759
|
+
};
|
|
760
|
+
|
|
729
761
|
if (journalSlug === PERSONAL_VAULT_JOURNAL_SLUG) migratePersonalVaultJournal();
|
|
730
762
|
const journal = migrateToV2(readJournal(journalSlug));
|
|
731
763
|
gcTombstones(journal, Date.now());
|
|
@@ -1454,7 +1486,8 @@ function finalizePullRun(
|
|
|
1454
1486
|
if (!run.options.skipReindex && changedOnDisk) {
|
|
1455
1487
|
try {
|
|
1456
1488
|
reindex({ repoRoot: run.hqRoot, skipLock: true });
|
|
1457
|
-
} catch {
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
logPostSyncMaintenanceFailure("post-sync reindex", run.hqRoot, err);
|
|
1458
1491
|
// best-effort: a post-sync refresh failure never fails the sync
|
|
1459
1492
|
}
|
|
1460
1493
|
}
|