@indigoai-us/hq-cloud 5.7.0 → 5.7.2
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/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +50 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/lib/conflict-file.d.ts +46 -0
- package/dist/lib/conflict-file.d.ts.map +1 -0
- package/dist/lib/conflict-file.js +86 -0
- package/dist/lib/conflict-file.js.map +1 -0
- package/dist/lib/conflict-index.d.ts +66 -0
- package/dist/lib/conflict-index.d.ts.map +1 -0
- package/dist/lib/conflict-index.js +112 -0
- package/dist/lib/conflict-index.js.map +1 -0
- package/dist/lib/conflict.test.d.ts +7 -0
- package/dist/lib/conflict.test.d.ts.map +1 -0
- package/dist/lib/conflict.test.js +136 -0
- package/dist/lib/conflict.test.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/sync.ts +66 -0
- package/src/lib/conflict-file.ts +101 -0
- package/src/lib/conflict-index.ts +127 -0
- package/src/lib/conflict.test.ts +180 -0
- package/src/types.ts +27 -0
package/dist/cli/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAe,MAAM,aAAa,CAAC;AAOnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAe,MAAM,aAAa,CAAC;AAOnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAQ1E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB,GACD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,UAAU,EAAE,kBAAkB,CAAC;CAChC,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,2BAA2B;IAC3B,WAAW,EAAE,kBAAkB,CAAC;IAChC,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuOpE"}
|
package/dist/cli/sync.js
CHANGED
|
@@ -11,6 +11,8 @@ import { downloadFile, listRemoteFiles } from "../s3.js";
|
|
|
11
11
|
import { readJournal, writeJournal, hashFile, updateEntry, getEntry, normalizeEtag } from "../journal.js";
|
|
12
12
|
import { createIgnoreFilter } from "../ignore.js";
|
|
13
13
|
import { resolveConflict } from "./conflict.js";
|
|
14
|
+
import { buildConflictId, buildConflictPath, readShortMachineId, } from "../lib/conflict-file.js";
|
|
15
|
+
import { appendConflictEntry } from "../lib/conflict-index.js";
|
|
14
16
|
/**
|
|
15
17
|
* Sync (pull) all allowed files from the entity vault.
|
|
16
18
|
*/
|
|
@@ -93,6 +95,39 @@ export async function sync(options) {
|
|
|
93
95
|
direction: "pull",
|
|
94
96
|
resolution,
|
|
95
97
|
});
|
|
98
|
+
// Write `<original>.conflict-<ts>-<machine>.<ext>` mirror + append to
|
|
99
|
+
// `<hqRoot>/.hq-conflicts/index.json` so the user can later run
|
|
100
|
+
// `/resolve-conflicts` to walk pending conflicts. Skipped for "abort"
|
|
101
|
+
// (user gave up) and "overwrite" (cloud bytes are about to replace
|
|
102
|
+
// local — mirror would be redundant). Best-effort: failure here only
|
|
103
|
+
// emits an error, doesn't break the sync.
|
|
104
|
+
if (resolution !== "abort" && resolution !== "overwrite") {
|
|
105
|
+
try {
|
|
106
|
+
const detectedAt = new Date().toISOString();
|
|
107
|
+
const machineId = readShortMachineId();
|
|
108
|
+
const originalRelative = path.relative(hqRoot, localPath);
|
|
109
|
+
const conflictRelative = buildConflictPath(originalRelative, detectedAt, machineId);
|
|
110
|
+
const conflictAbs = path.join(hqRoot, conflictRelative);
|
|
111
|
+
await downloadFile(ctx, remoteFile.key, conflictAbs);
|
|
112
|
+
appendConflictEntry(hqRoot, {
|
|
113
|
+
id: buildConflictId(originalRelative, detectedAt),
|
|
114
|
+
originalPath: originalRelative,
|
|
115
|
+
conflictPath: conflictRelative,
|
|
116
|
+
detectedAt,
|
|
117
|
+
side: "pull",
|
|
118
|
+
machineId,
|
|
119
|
+
localHash: item.localHash,
|
|
120
|
+
remoteHash: remoteFile.etag ? normalizeEtag(remoteFile.etag) : "",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (mirrorErr) {
|
|
124
|
+
emit({
|
|
125
|
+
type: "error",
|
|
126
|
+
path: remoteFile.key,
|
|
127
|
+
message: `conflict mirror write failed: ${mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
96
131
|
if (resolution === "abort") {
|
|
97
132
|
writeJournal(journalSlug, journal);
|
|
98
133
|
return {
|
|
@@ -106,6 +141,21 @@ export async function sync(options) {
|
|
|
106
141
|
}
|
|
107
142
|
if (resolution === "keep" || resolution === "skip") {
|
|
108
143
|
filesSkipped++;
|
|
144
|
+
// Stamp the journal with the new baseline so the same conflict
|
|
145
|
+
// doesn't re-fire on every subsequent sync. After "keep", local
|
|
146
|
+
// wins — the user has accepted that the cloud version we just
|
|
147
|
+
// mirrored is what cloud is at this etag, and they don't want
|
|
148
|
+
// it. Recording (current localHash + current remoteEtag) tells
|
|
149
|
+
// the next sync "no change on either side" until something new
|
|
150
|
+
// diverges. Without this, both `localChanged` and `remoteChanged`
|
|
151
|
+
// stay true forever and the conflict is sticky.
|
|
152
|
+
try {
|
|
153
|
+
const stat = fs.statSync(localPath);
|
|
154
|
+
updateEntry(journal, remoteFile.key, item.localHash, stat.size, "down", remoteFile.etag);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// best-effort — sync continues even if stat fails
|
|
158
|
+
}
|
|
109
159
|
continue;
|
|
110
160
|
}
|
|
111
161
|
// "overwrite" falls through to download
|
package/dist/cli/sync.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/cli/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict file naming + writing.
|
|
3
|
+
*
|
|
4
|
+
* When share/sync detects divergence, the cloud's version of the file is
|
|
5
|
+
* written next to the original with a name encoding the timestamp and the
|
|
6
|
+
* machine that detected the conflict. Lets multiple machines independently
|
|
7
|
+
* surface their own conflicts without name collisions, and lets the user
|
|
8
|
+
* (or the `/resolve-conflicts` HQ skill) see local + cloud side-by-side
|
|
9
|
+
* in their file browser.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Read the short machine ID (first 6 chars) from `~/.hq/menubar.json`.
|
|
13
|
+
* Falls back to "unknown" if the file is missing/unreadable — conflict
|
|
14
|
+
* files should still be written even when machine identity is unclear.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readShortMachineId(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Build the conflict file path for an original. ISO uses `-` instead of
|
|
19
|
+
* `:` so the result is filesystem-safe on every OS, and the original
|
|
20
|
+
* extension is preserved at the end so editors syntax-highlight correctly.
|
|
21
|
+
*
|
|
22
|
+
* knowledge/notes.md, 2026-04-27T22:05:14Z, abc123
|
|
23
|
+
* → knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md
|
|
24
|
+
*
|
|
25
|
+
* projects/foo/prd.json, ..., abc123
|
|
26
|
+
* → projects/foo/prd.json.conflict-...-abc123.json
|
|
27
|
+
*
|
|
28
|
+
* Files without an extension get the `.conflict-...` suffix appended verbatim.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildConflictPath(originalRelative: string, detectedAt: string, shortMachineId: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Write the cloud-side bytes to the conflict path. Creates parent dirs as
|
|
33
|
+
* needed (the conflict file always lives next to the original, so the
|
|
34
|
+
* parent already exists in the steady-state — but defense-in-depth).
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeConflictFile(hqRoot: string, conflictRelative: string, contents: Buffer): void;
|
|
37
|
+
/**
|
|
38
|
+
* Stable conflict ID — used to dedupe re-detections of the same conflict.
|
|
39
|
+
* Re-running sync after a conflict but before the user has resolved should
|
|
40
|
+
* NOT pile up duplicate entries. The id is derived from the original path
|
|
41
|
+
* and the detection timestamp; if the same original conflicts twice with
|
|
42
|
+
* the user resolving in between, that's a new id (different timestamp),
|
|
43
|
+
* which is correct.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildConflictId(originalRelative: string, detectedAt: string): string;
|
|
46
|
+
//# sourceMappingURL=conflict-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict-file.d.ts","sourceRoot":"","sources":["../../src/lib/conflict-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAiBH;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAS3C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,gBAAgB,EAAE,MAAM,EACxB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,GACrB,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,GACf,IAAI,CAIN;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,gBAAgB,EAAE,MAAM,EACxB,UAAU,EAAE,MAAM,GACjB,MAAM,CAIR"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict file naming + writing.
|
|
3
|
+
*
|
|
4
|
+
* When share/sync detects divergence, the cloud's version of the file is
|
|
5
|
+
* written next to the original with a name encoding the timestamp and the
|
|
6
|
+
* machine that detected the conflict. Lets multiple machines independently
|
|
7
|
+
* surface their own conflicts without name collisions, and lets the user
|
|
8
|
+
* (or the `/resolve-conflicts` HQ skill) see local + cloud side-by-side
|
|
9
|
+
* in their file browser.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
/**
|
|
15
|
+
* Path to `~/.hq/menubar.json`. Evaluated lazily at call time (not module
|
|
16
|
+
* load) so that tests overriding `HOME` after import — and any future code
|
|
17
|
+
* that changes the user's effective home dir at runtime — see the right
|
|
18
|
+
* file. Going through `os.homedir()` rather than `process.env.HOME` keeps
|
|
19
|
+
* the Windows USERPROFILE fallback intact.
|
|
20
|
+
*/
|
|
21
|
+
function menubarJsonPath() {
|
|
22
|
+
return path.join(os.homedir(), ".hq", "menubar.json");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read the short machine ID (first 6 chars) from `~/.hq/menubar.json`.
|
|
26
|
+
* Falls back to "unknown" if the file is missing/unreadable — conflict
|
|
27
|
+
* files should still be written even when machine identity is unclear.
|
|
28
|
+
*/
|
|
29
|
+
export function readShortMachineId() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(menubarJsonPath(), "utf-8");
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
const id = typeof parsed.machineId === "string" ? parsed.machineId : "";
|
|
34
|
+
return id.slice(0, 6) || "unknown";
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the conflict file path for an original. ISO uses `-` instead of
|
|
42
|
+
* `:` so the result is filesystem-safe on every OS, and the original
|
|
43
|
+
* extension is preserved at the end so editors syntax-highlight correctly.
|
|
44
|
+
*
|
|
45
|
+
* knowledge/notes.md, 2026-04-27T22:05:14Z, abc123
|
|
46
|
+
* → knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md
|
|
47
|
+
*
|
|
48
|
+
* projects/foo/prd.json, ..., abc123
|
|
49
|
+
* → projects/foo/prd.json.conflict-...-abc123.json
|
|
50
|
+
*
|
|
51
|
+
* Files without an extension get the `.conflict-...` suffix appended verbatim.
|
|
52
|
+
*/
|
|
53
|
+
export function buildConflictPath(originalRelative, detectedAt, shortMachineId) {
|
|
54
|
+
const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
|
|
55
|
+
const ext = path.extname(originalRelative); // ".md" or "" if none
|
|
56
|
+
// The full original path is preserved (extension and all) so users can
|
|
57
|
+
// visually pair `notes.md` with `notes.md.conflict-…md` in their file
|
|
58
|
+
// browser. The trailing `<ext>` after the timestamp keeps the file
|
|
59
|
+
// syntax-highlighted in editors that key off the final extension.
|
|
60
|
+
const suffix = `.conflict-${safeTs}-${shortMachineId}${ext}`;
|
|
61
|
+
return `${originalRelative}${suffix}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Write the cloud-side bytes to the conflict path. Creates parent dirs as
|
|
65
|
+
* needed (the conflict file always lives next to the original, so the
|
|
66
|
+
* parent already exists in the steady-state — but defense-in-depth).
|
|
67
|
+
*/
|
|
68
|
+
export function writeConflictFile(hqRoot, conflictRelative, contents) {
|
|
69
|
+
const abs = path.join(hqRoot, conflictRelative);
|
|
70
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
71
|
+
fs.writeFileSync(abs, contents);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stable conflict ID — used to dedupe re-detections of the same conflict.
|
|
75
|
+
* Re-running sync after a conflict but before the user has resolved should
|
|
76
|
+
* NOT pile up duplicate entries. The id is derived from the original path
|
|
77
|
+
* and the detection timestamp; if the same original conflicts twice with
|
|
78
|
+
* the user resolving in between, that's a new id (different timestamp),
|
|
79
|
+
* which is correct.
|
|
80
|
+
*/
|
|
81
|
+
export function buildConflictId(originalRelative, detectedAt) {
|
|
82
|
+
const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
|
|
83
|
+
const safePath = originalRelative.replace(/[\/\\.]/g, "-");
|
|
84
|
+
return `${safePath}-${safeTs}`;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=conflict-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict-file.js","sourceRoot":"","sources":["../../src/lib/conflict-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B;;;;;;GAMG;AACH,SAAS,eAAe;IACtB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,EAAE,OAAO,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,EAAE,GAAG,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACxE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAC/B,gBAAwB,EACxB,UAAkB,EAClB,cAAsB;IAEtB,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAClE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,sBAAsB;IAClE,uEAAuE;IACvE,sEAAsE;IACtE,mEAAmE;IACnE,kEAAkE;IAClE,MAAM,MAAM,GAAG,aAAa,MAAM,IAAI,cAAc,GAAG,GAAG,EAAE,CAAC;IAC7D,OAAO,GAAG,gBAAgB,GAAG,MAAM,EAAE,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,gBAAwB,EACxB,QAAgB;IAEhB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,gBAAwB,EACxB,UAAkB;IAElB,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IAC3D,OAAO,GAAG,QAAQ,IAAI,MAAM,EAAE,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict index — durable record of pending divergences awaiting resolution.
|
|
3
|
+
*
|
|
4
|
+
* Lives at `<hq_root>/.hq-conflicts/index.json` (inside HQ content, NOT in
|
|
5
|
+
* `~/.hq/`). Two reasons it sits in HQ content rather than in the state dir:
|
|
6
|
+
* 1. The `/resolve-conflicts` HQ skill discovers it relative to the user's
|
|
7
|
+
* HQ folder — that's the user's mental anchor for "where my files are."
|
|
8
|
+
* 2. The conflict-side files themselves live in HQ content, so the index
|
|
9
|
+
* and the files it references stay co-located. If the user moves HQ,
|
|
10
|
+
* the index moves with it.
|
|
11
|
+
*
|
|
12
|
+
* Excluded from cross-machine sync via `.hqignore` — each machine resolves
|
|
13
|
+
* its own queue. We never propagate conflict files (they'd just create more
|
|
14
|
+
* conflicts on the other side).
|
|
15
|
+
*
|
|
16
|
+
* Writes are atomic (tmp + rename). The resolution skill mutates this file
|
|
17
|
+
* mid-walk; a torn write would corrupt the only record of pending conflicts
|
|
18
|
+
* and could lose track of files we'd written to disk. Higher stakes than the
|
|
19
|
+
* journal, which the next sync can rebuild.
|
|
20
|
+
*/
|
|
21
|
+
import type { ConflictIndex, ConflictIndexEntry } from "../types.js";
|
|
22
|
+
/**
|
|
23
|
+
* Absolute path to the conflict index for a given HQ root.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getConflictIndexPath(hqRoot: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Read the conflict index. Returns an empty index if the file doesn't exist
|
|
28
|
+
* yet (first-conflict-ever case).
|
|
29
|
+
*
|
|
30
|
+
* Throws on corrupt JSON — we deliberately don't auto-repair, since the only
|
|
31
|
+
* record of pending conflicts is too important to silently overwrite. The
|
|
32
|
+
* `/resolve-conflicts` skill surfaces this case to the user with a manual
|
|
33
|
+
* inspection prompt.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readConflictIndex(hqRoot: string): ConflictIndex;
|
|
36
|
+
/**
|
|
37
|
+
* Atomically write the conflict index. Writes to `<index>.tmp.<random>` then
|
|
38
|
+
* renames into place — `rename(2)` is atomic on POSIX, so a crash mid-write
|
|
39
|
+
* leaves either the old file or the new one, never a half-written one.
|
|
40
|
+
*
|
|
41
|
+
* Always sorts conflicts by `detectedAt` ascending before writing — keeps
|
|
42
|
+
* the file diff-friendly across runs and makes "oldest-first walk" the
|
|
43
|
+
* natural read order in the resolution skill.
|
|
44
|
+
*/
|
|
45
|
+
export declare function writeConflictIndex(hqRoot: string, index: ConflictIndex): void;
|
|
46
|
+
/**
|
|
47
|
+
* Idempotent append. If an entry with the same `id` already exists (same
|
|
48
|
+
* original path, same detection timestamp), update it in place rather than
|
|
49
|
+
* duplicating. This matters because re-running sync after a conflict but
|
|
50
|
+
* before resolution will re-detect the same divergence — without dedup the
|
|
51
|
+
* index would grow unboundedly.
|
|
52
|
+
*
|
|
53
|
+
* The "update in place" path also covers the case where the cloud advanced
|
|
54
|
+
* again between detections: we want the latest `remoteVersionId` and
|
|
55
|
+
* `remoteHash` so the resolution skill shows the user the *current* cloud
|
|
56
|
+
* state, not stale data from the first detection.
|
|
57
|
+
*/
|
|
58
|
+
export declare function appendConflictEntry(hqRoot: string, entry: ConflictIndexEntry): void;
|
|
59
|
+
/**
|
|
60
|
+
* Remove an entry by id. Used by the `/resolve-conflicts` skill after the
|
|
61
|
+
* user picks a resolution and the conflict file is cleaned up. No-op if the
|
|
62
|
+
* id isn't present (e.g. user manually removed the file then re-ran the
|
|
63
|
+
* skill — we want that to be a clean exit, not an error).
|
|
64
|
+
*/
|
|
65
|
+
export declare function removeConflictEntry(hqRoot: string, id: string): void;
|
|
66
|
+
//# sourceMappingURL=conflict-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict-index.d.ts","sourceRoot":"","sources":["../../src/lib/conflict-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKrE;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAY/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,GACnB,IAAI,CAgBN;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,kBAAkB,GACxB,IAAI,CASN;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAKpE"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict index — durable record of pending divergences awaiting resolution.
|
|
3
|
+
*
|
|
4
|
+
* Lives at `<hq_root>/.hq-conflicts/index.json` (inside HQ content, NOT in
|
|
5
|
+
* `~/.hq/`). Two reasons it sits in HQ content rather than in the state dir:
|
|
6
|
+
* 1. The `/resolve-conflicts` HQ skill discovers it relative to the user's
|
|
7
|
+
* HQ folder — that's the user's mental anchor for "where my files are."
|
|
8
|
+
* 2. The conflict-side files themselves live in HQ content, so the index
|
|
9
|
+
* and the files it references stay co-located. If the user moves HQ,
|
|
10
|
+
* the index moves with it.
|
|
11
|
+
*
|
|
12
|
+
* Excluded from cross-machine sync via `.hqignore` — each machine resolves
|
|
13
|
+
* its own queue. We never propagate conflict files (they'd just create more
|
|
14
|
+
* conflicts on the other side).
|
|
15
|
+
*
|
|
16
|
+
* Writes are atomic (tmp + rename). The resolution skill mutates this file
|
|
17
|
+
* mid-walk; a torn write would corrupt the only record of pending conflicts
|
|
18
|
+
* and could lose track of files we'd written to disk. Higher stakes than the
|
|
19
|
+
* journal, which the next sync can rebuild.
|
|
20
|
+
*/
|
|
21
|
+
import * as crypto from "crypto";
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
const CONFLICTS_DIR = ".hq-conflicts";
|
|
25
|
+
const INDEX_FILENAME = "index.json";
|
|
26
|
+
/**
|
|
27
|
+
* Absolute path to the conflict index for a given HQ root.
|
|
28
|
+
*/
|
|
29
|
+
export function getConflictIndexPath(hqRoot) {
|
|
30
|
+
return path.join(hqRoot, CONFLICTS_DIR, INDEX_FILENAME);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read the conflict index. Returns an empty index if the file doesn't exist
|
|
34
|
+
* yet (first-conflict-ever case).
|
|
35
|
+
*
|
|
36
|
+
* Throws on corrupt JSON — we deliberately don't auto-repair, since the only
|
|
37
|
+
* record of pending conflicts is too important to silently overwrite. The
|
|
38
|
+
* `/resolve-conflicts` skill surfaces this case to the user with a manual
|
|
39
|
+
* inspection prompt.
|
|
40
|
+
*/
|
|
41
|
+
export function readConflictIndex(hqRoot) {
|
|
42
|
+
const indexPath = getConflictIndexPath(hqRoot);
|
|
43
|
+
if (!fs.existsSync(indexPath)) {
|
|
44
|
+
return { version: 1, conflicts: [] };
|
|
45
|
+
}
|
|
46
|
+
const raw = fs.readFileSync(indexPath, "utf-8");
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
// Defensive: an empty file or wrong-shape JSON shouldn't crash callers.
|
|
49
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.conflicts)) {
|
|
50
|
+
return { version: 1, conflicts: [] };
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Atomically write the conflict index. Writes to `<index>.tmp.<random>` then
|
|
56
|
+
* renames into place — `rename(2)` is atomic on POSIX, so a crash mid-write
|
|
57
|
+
* leaves either the old file or the new one, never a half-written one.
|
|
58
|
+
*
|
|
59
|
+
* Always sorts conflicts by `detectedAt` ascending before writing — keeps
|
|
60
|
+
* the file diff-friendly across runs and makes "oldest-first walk" the
|
|
61
|
+
* natural read order in the resolution skill.
|
|
62
|
+
*/
|
|
63
|
+
export function writeConflictIndex(hqRoot, index) {
|
|
64
|
+
const indexPath = getConflictIndexPath(hqRoot);
|
|
65
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
66
|
+
const sorted = {
|
|
67
|
+
version: index.version,
|
|
68
|
+
conflicts: [...index.conflicts].sort((a, b) => a.detectedAt.localeCompare(b.detectedAt)),
|
|
69
|
+
};
|
|
70
|
+
// Random suffix in tmp name avoids collision if two sync runs ever overlap
|
|
71
|
+
// (shouldn't happen — the runner serializes — but cheap insurance).
|
|
72
|
+
const tmpPath = `${indexPath}.tmp.${crypto.randomBytes(6).toString("hex")}`;
|
|
73
|
+
fs.writeFileSync(tmpPath, JSON.stringify(sorted, null, 2));
|
|
74
|
+
fs.renameSync(tmpPath, indexPath);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Idempotent append. If an entry with the same `id` already exists (same
|
|
78
|
+
* original path, same detection timestamp), update it in place rather than
|
|
79
|
+
* duplicating. This matters because re-running sync after a conflict but
|
|
80
|
+
* before resolution will re-detect the same divergence — without dedup the
|
|
81
|
+
* index would grow unboundedly.
|
|
82
|
+
*
|
|
83
|
+
* The "update in place" path also covers the case where the cloud advanced
|
|
84
|
+
* again between detections: we want the latest `remoteVersionId` and
|
|
85
|
+
* `remoteHash` so the resolution skill shows the user the *current* cloud
|
|
86
|
+
* state, not stale data from the first detection.
|
|
87
|
+
*/
|
|
88
|
+
export function appendConflictEntry(hqRoot, entry) {
|
|
89
|
+
const index = readConflictIndex(hqRoot);
|
|
90
|
+
const existingIdx = index.conflicts.findIndex((c) => c.id === entry.id);
|
|
91
|
+
if (existingIdx >= 0) {
|
|
92
|
+
index.conflicts[existingIdx] = entry;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
index.conflicts.push(entry);
|
|
96
|
+
}
|
|
97
|
+
writeConflictIndex(hqRoot, index);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Remove an entry by id. Used by the `/resolve-conflicts` skill after the
|
|
101
|
+
* user picks a resolution and the conflict file is cleaned up. No-op if the
|
|
102
|
+
* id isn't present (e.g. user manually removed the file then re-ran the
|
|
103
|
+
* skill — we want that to be a clean exit, not an error).
|
|
104
|
+
*/
|
|
105
|
+
export function removeConflictEntry(hqRoot, id) {
|
|
106
|
+
const index = readConflictIndex(hqRoot);
|
|
107
|
+
const filtered = index.conflicts.filter((c) => c.id !== id);
|
|
108
|
+
if (filtered.length === index.conflicts.length)
|
|
109
|
+
return;
|
|
110
|
+
writeConflictIndex(hqRoot, { version: index.version, conflicts: filtered });
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=conflict-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict-index.js","sourceRoot":"","sources":["../../src/lib/conflict-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict.test.d.ts","sourceRoot":"","sources":["../../src/lib/conflict.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the pure conflict primitives — path building, machine-id
|
|
3
|
+
* fallback, atomic index writes, dedup. Kept in one file so the related
|
|
4
|
+
* helpers stay co-located.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as os from "os";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import { buildConflictPath, buildConflictId, readShortMachineId, } from "./conflict-file.js";
|
|
11
|
+
import { appendConflictEntry, getConflictIndexPath, readConflictIndex, removeConflictEntry, writeConflictIndex, } from "./conflict-index.js";
|
|
12
|
+
describe("buildConflictPath", () => {
|
|
13
|
+
it("inserts the conflict marker before the original extension", () => {
|
|
14
|
+
expect(buildConflictPath("knowledge/notes.md", "2026-04-27T22:05:14Z", "abc123")).toBe("knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md");
|
|
15
|
+
});
|
|
16
|
+
it("preserves nested paths and json extensions", () => {
|
|
17
|
+
expect(buildConflictPath("projects/foo/prd.json", "2026-04-27T22:05:14.123Z", "abc123")).toBe("projects/foo/prd.json.conflict-2026-04-27T22-05-14Z-abc123.json");
|
|
18
|
+
});
|
|
19
|
+
it("appends the suffix verbatim for files without an extension", () => {
|
|
20
|
+
expect(buildConflictPath("secrets", "2026-04-27T22:05:14Z", "abc123")).toBe("secrets.conflict-2026-04-27T22-05-14Z-abc123");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("buildConflictId", () => {
|
|
24
|
+
it("escapes path separators and dots so the id is filesystem-safe", () => {
|
|
25
|
+
expect(buildConflictId("knowledge/notes.md", "2026-04-27T22:05:14Z")).toBe("knowledge-notes-md-2026-04-27T22-05-14Z");
|
|
26
|
+
});
|
|
27
|
+
it("yields the same id for the same (path, ts) pair — dedup primitive", () => {
|
|
28
|
+
const a = buildConflictId("foo/bar.md", "2026-04-27T22:05:14Z");
|
|
29
|
+
const b = buildConflictId("foo/bar.md", "2026-04-27T22:05:14Z");
|
|
30
|
+
expect(a).toBe(b);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("readShortMachineId", () => {
|
|
34
|
+
let originalHome;
|
|
35
|
+
let tmpHome;
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
originalHome = process.env.HOME;
|
|
38
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "hq-machineid-"));
|
|
39
|
+
process.env.HOME = tmpHome;
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
if (originalHome)
|
|
43
|
+
process.env.HOME = originalHome;
|
|
44
|
+
else
|
|
45
|
+
delete process.env.HOME;
|
|
46
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
it("returns the first 6 chars when menubar.json has a machineId", () => {
|
|
49
|
+
fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
|
|
50
|
+
fs.writeFileSync(path.join(tmpHome, ".hq", "menubar.json"), JSON.stringify({ machineId: "deadbeefcafe1234567890" }));
|
|
51
|
+
expect(readShortMachineId()).toBe("deadbe");
|
|
52
|
+
});
|
|
53
|
+
it("falls back to 'unknown' when menubar.json is missing", () => {
|
|
54
|
+
expect(readShortMachineId()).toBe("unknown");
|
|
55
|
+
});
|
|
56
|
+
it("falls back to 'unknown' when menubar.json is malformed", () => {
|
|
57
|
+
fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
|
|
58
|
+
fs.writeFileSync(path.join(tmpHome, ".hq", "menubar.json"), "{not-json");
|
|
59
|
+
expect(readShortMachineId()).toBe("unknown");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("conflict index", () => {
|
|
63
|
+
let tmpHq;
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
tmpHq = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cidx-"));
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
fs.rmSync(tmpHq, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
function entry(overrides = {}) {
|
|
71
|
+
return {
|
|
72
|
+
id: "knowledge-notes-md-2026-04-27T22-05-14Z",
|
|
73
|
+
originalPath: "knowledge/notes.md",
|
|
74
|
+
conflictPath: "knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md",
|
|
75
|
+
detectedAt: "2026-04-27T22:05:14Z",
|
|
76
|
+
side: "pull",
|
|
77
|
+
machineId: "abc123",
|
|
78
|
+
localHash: "local",
|
|
79
|
+
remoteHash: "remote",
|
|
80
|
+
remoteVersionId: "v2",
|
|
81
|
+
lastKnownVersionId: "v1",
|
|
82
|
+
...overrides,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
it("returns an empty index when the file does not exist", () => {
|
|
86
|
+
const idx = readConflictIndex(tmpHq);
|
|
87
|
+
expect(idx).toEqual({ version: 1, conflicts: [] });
|
|
88
|
+
});
|
|
89
|
+
it("creates the .hq-conflicts dir on first write", () => {
|
|
90
|
+
appendConflictEntry(tmpHq, entry());
|
|
91
|
+
expect(fs.existsSync(getConflictIndexPath(tmpHq))).toBe(true);
|
|
92
|
+
expect(fs.existsSync(path.join(tmpHq, ".hq-conflicts"))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("appends new entries and dedupes on id (idempotent re-detection)", () => {
|
|
95
|
+
appendConflictEntry(tmpHq, entry({ id: "a", remoteVersionId: "v1" }));
|
|
96
|
+
appendConflictEntry(tmpHq, entry({ id: "b", remoteVersionId: "v1" }));
|
|
97
|
+
// Re-detect "a" — the second push should update in place, not duplicate.
|
|
98
|
+
appendConflictEntry(tmpHq, entry({ id: "a", remoteVersionId: "v9" }));
|
|
99
|
+
const idx = readConflictIndex(tmpHq);
|
|
100
|
+
expect(idx.conflicts).toHaveLength(2);
|
|
101
|
+
const a = idx.conflicts.find((c) => c.id === "a");
|
|
102
|
+
expect(a?.remoteVersionId).toBe("v9"); // updated, not appended
|
|
103
|
+
});
|
|
104
|
+
it("sorts conflicts by detectedAt ascending on every write", () => {
|
|
105
|
+
appendConflictEntry(tmpHq, entry({ id: "newer", detectedAt: "2026-04-27T23:00:00Z" }));
|
|
106
|
+
appendConflictEntry(tmpHq, entry({ id: "older", detectedAt: "2026-04-27T22:00:00Z" }));
|
|
107
|
+
const idx = readConflictIndex(tmpHq);
|
|
108
|
+
expect(idx.conflicts.map((c) => c.id)).toEqual(["older", "newer"]);
|
|
109
|
+
});
|
|
110
|
+
it("removeConflictEntry removes a single entry by id", () => {
|
|
111
|
+
appendConflictEntry(tmpHq, entry({ id: "keep" }));
|
|
112
|
+
appendConflictEntry(tmpHq, entry({ id: "drop" }));
|
|
113
|
+
removeConflictEntry(tmpHq, "drop");
|
|
114
|
+
const idx = readConflictIndex(tmpHq);
|
|
115
|
+
expect(idx.conflicts.map((c) => c.id)).toEqual(["keep"]);
|
|
116
|
+
});
|
|
117
|
+
it("removeConflictEntry is a no-op when the id is not present", () => {
|
|
118
|
+
appendConflictEntry(tmpHq, entry({ id: "a" }));
|
|
119
|
+
expect(() => removeConflictEntry(tmpHq, "missing")).not.toThrow();
|
|
120
|
+
expect(readConflictIndex(tmpHq).conflicts).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
it("writeConflictIndex leaves no .tmp files on disk after success", () => {
|
|
123
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [entry()] });
|
|
124
|
+
const files = fs.readdirSync(path.join(tmpHq, ".hq-conflicts"));
|
|
125
|
+
// Only index.json should remain; tmp files renamed atomically into place.
|
|
126
|
+
expect(files).toEqual(["index.json"]);
|
|
127
|
+
});
|
|
128
|
+
it("returns an empty index for malformed-but-parseable JSON", () => {
|
|
129
|
+
const indexPath = getConflictIndexPath(tmpHq);
|
|
130
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
131
|
+
fs.writeFileSync(indexPath, JSON.stringify({ version: 1 })); // no `conflicts` array
|
|
132
|
+
const idx = readConflictIndex(tmpHq);
|
|
133
|
+
expect(idx.conflicts).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=conflict.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict.test.js","sourceRoot":"","sources":["../../src/lib/conflict.test.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -91,4 +91,22 @@ export interface VaultServiceConfig {
|
|
|
91
91
|
/** AWS region for S3 client (defaults to entity region or us-east-1) */
|
|
92
92
|
region?: string;
|
|
93
93
|
}
|
|
94
|
+
export interface ConflictIndexEntry {
|
|
95
|
+
id: string;
|
|
96
|
+
originalPath: string;
|
|
97
|
+
conflictPath: string;
|
|
98
|
+
detectedAt: string;
|
|
99
|
+
side: "push" | "pull";
|
|
100
|
+
machineId: string;
|
|
101
|
+
localHash: string;
|
|
102
|
+
remoteHash: string;
|
|
103
|
+
/** S3 VersionId when known (present for VersionId-aware buckets). */
|
|
104
|
+
remoteVersionId?: string;
|
|
105
|
+
/** Last-known parent VersionId from journal, when known. */
|
|
106
|
+
lastKnownVersionId?: string | null;
|
|
107
|
+
}
|
|
108
|
+
export interface ConflictIndex {
|
|
109
|
+
version: 1;
|
|
110
|
+
conflicts: ConflictIndexEntry[];
|
|
111
|
+
}
|
|
94
112
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,GAAG,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,WAAW,EAAE,gBAAgB,CAAC;IAC9B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/cli/sync.ts
CHANGED
|
@@ -15,6 +15,12 @@ import { readJournal, writeJournal, hashFile, updateEntry, getEntry, normalizeEt
|
|
|
15
15
|
import { createIgnoreFilter } from "../ignore.js";
|
|
16
16
|
import { resolveConflict } from "./conflict.js";
|
|
17
17
|
import type { ConflictStrategy, ConflictResolution } from "./conflict.js";
|
|
18
|
+
import {
|
|
19
|
+
buildConflictId,
|
|
20
|
+
buildConflictPath,
|
|
21
|
+
readShortMachineId,
|
|
22
|
+
} from "../lib/conflict-file.js";
|
|
23
|
+
import { appendConflictEntry } from "../lib/conflict-index.js";
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Per-file events emitted by `sync()` as it progresses.
|
|
@@ -213,6 +219,45 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
|
|
|
213
219
|
resolution,
|
|
214
220
|
});
|
|
215
221
|
|
|
222
|
+
// Write `<original>.conflict-<ts>-<machine>.<ext>` mirror + append to
|
|
223
|
+
// `<hqRoot>/.hq-conflicts/index.json` so the user can later run
|
|
224
|
+
// `/resolve-conflicts` to walk pending conflicts. Skipped for "abort"
|
|
225
|
+
// (user gave up) and "overwrite" (cloud bytes are about to replace
|
|
226
|
+
// local — mirror would be redundant). Best-effort: failure here only
|
|
227
|
+
// emits an error, doesn't break the sync.
|
|
228
|
+
if (resolution !== "abort" && resolution !== "overwrite") {
|
|
229
|
+
try {
|
|
230
|
+
const detectedAt = new Date().toISOString();
|
|
231
|
+
const machineId = readShortMachineId();
|
|
232
|
+
const originalRelative = path.relative(hqRoot, localPath);
|
|
233
|
+
const conflictRelative = buildConflictPath(
|
|
234
|
+
originalRelative,
|
|
235
|
+
detectedAt,
|
|
236
|
+
machineId,
|
|
237
|
+
);
|
|
238
|
+
const conflictAbs = path.join(hqRoot, conflictRelative);
|
|
239
|
+
await downloadFile(ctx, remoteFile.key, conflictAbs);
|
|
240
|
+
appendConflictEntry(hqRoot, {
|
|
241
|
+
id: buildConflictId(originalRelative, detectedAt),
|
|
242
|
+
originalPath: originalRelative,
|
|
243
|
+
conflictPath: conflictRelative,
|
|
244
|
+
detectedAt,
|
|
245
|
+
side: "pull",
|
|
246
|
+
machineId,
|
|
247
|
+
localHash: item.localHash,
|
|
248
|
+
remoteHash: remoteFile.etag ? normalizeEtag(remoteFile.etag) : "",
|
|
249
|
+
});
|
|
250
|
+
} catch (mirrorErr) {
|
|
251
|
+
emit({
|
|
252
|
+
type: "error",
|
|
253
|
+
path: remoteFile.key,
|
|
254
|
+
message: `conflict mirror write failed: ${
|
|
255
|
+
mirrorErr instanceof Error ? mirrorErr.message : String(mirrorErr)
|
|
256
|
+
}`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
216
261
|
if (resolution === "abort") {
|
|
217
262
|
writeJournal(journalSlug, journal);
|
|
218
263
|
return {
|
|
@@ -226,6 +271,27 @@ export async function sync(options: SyncOptions): Promise<SyncResult> {
|
|
|
226
271
|
}
|
|
227
272
|
if (resolution === "keep" || resolution === "skip") {
|
|
228
273
|
filesSkipped++;
|
|
274
|
+
// Stamp the journal with the new baseline so the same conflict
|
|
275
|
+
// doesn't re-fire on every subsequent sync. After "keep", local
|
|
276
|
+
// wins — the user has accepted that the cloud version we just
|
|
277
|
+
// mirrored is what cloud is at this etag, and they don't want
|
|
278
|
+
// it. Recording (current localHash + current remoteEtag) tells
|
|
279
|
+
// the next sync "no change on either side" until something new
|
|
280
|
+
// diverges. Without this, both `localChanged` and `remoteChanged`
|
|
281
|
+
// stay true forever and the conflict is sticky.
|
|
282
|
+
try {
|
|
283
|
+
const stat = fs.statSync(localPath);
|
|
284
|
+
updateEntry(
|
|
285
|
+
journal,
|
|
286
|
+
remoteFile.key,
|
|
287
|
+
item.localHash,
|
|
288
|
+
stat.size,
|
|
289
|
+
"down",
|
|
290
|
+
remoteFile.etag,
|
|
291
|
+
);
|
|
292
|
+
} catch {
|
|
293
|
+
// best-effort — sync continues even if stat fails
|
|
294
|
+
}
|
|
229
295
|
continue;
|
|
230
296
|
}
|
|
231
297
|
// "overwrite" falls through to download
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict file naming + writing.
|
|
3
|
+
*
|
|
4
|
+
* When share/sync detects divergence, the cloud's version of the file is
|
|
5
|
+
* written next to the original with a name encoding the timestamp and the
|
|
6
|
+
* machine that detected the conflict. Lets multiple machines independently
|
|
7
|
+
* surface their own conflicts without name collisions, and lets the user
|
|
8
|
+
* (or the `/resolve-conflicts` HQ skill) see local + cloud side-by-side
|
|
9
|
+
* in their file browser.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as os from "os";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Path to `~/.hq/menubar.json`. Evaluated lazily at call time (not module
|
|
18
|
+
* load) so that tests overriding `HOME` after import — and any future code
|
|
19
|
+
* that changes the user's effective home dir at runtime — see the right
|
|
20
|
+
* file. Going through `os.homedir()` rather than `process.env.HOME` keeps
|
|
21
|
+
* the Windows USERPROFILE fallback intact.
|
|
22
|
+
*/
|
|
23
|
+
function menubarJsonPath(): string {
|
|
24
|
+
return path.join(os.homedir(), ".hq", "menubar.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read the short machine ID (first 6 chars) from `~/.hq/menubar.json`.
|
|
29
|
+
* Falls back to "unknown" if the file is missing/unreadable — conflict
|
|
30
|
+
* files should still be written even when machine identity is unclear.
|
|
31
|
+
*/
|
|
32
|
+
export function readShortMachineId(): string {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(menubarJsonPath(), "utf-8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
const id = typeof parsed.machineId === "string" ? parsed.machineId : "";
|
|
37
|
+
return id.slice(0, 6) || "unknown";
|
|
38
|
+
} catch {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the conflict file path for an original. ISO uses `-` instead of
|
|
45
|
+
* `:` so the result is filesystem-safe on every OS, and the original
|
|
46
|
+
* extension is preserved at the end so editors syntax-highlight correctly.
|
|
47
|
+
*
|
|
48
|
+
* knowledge/notes.md, 2026-04-27T22:05:14Z, abc123
|
|
49
|
+
* → knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md
|
|
50
|
+
*
|
|
51
|
+
* projects/foo/prd.json, ..., abc123
|
|
52
|
+
* → projects/foo/prd.json.conflict-...-abc123.json
|
|
53
|
+
*
|
|
54
|
+
* Files without an extension get the `.conflict-...` suffix appended verbatim.
|
|
55
|
+
*/
|
|
56
|
+
export function buildConflictPath(
|
|
57
|
+
originalRelative: string,
|
|
58
|
+
detectedAt: string,
|
|
59
|
+
shortMachineId: string,
|
|
60
|
+
): string {
|
|
61
|
+
const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
|
|
62
|
+
const ext = path.extname(originalRelative); // ".md" or "" if none
|
|
63
|
+
// The full original path is preserved (extension and all) so users can
|
|
64
|
+
// visually pair `notes.md` with `notes.md.conflict-…md` in their file
|
|
65
|
+
// browser. The trailing `<ext>` after the timestamp keeps the file
|
|
66
|
+
// syntax-highlighted in editors that key off the final extension.
|
|
67
|
+
const suffix = `.conflict-${safeTs}-${shortMachineId}${ext}`;
|
|
68
|
+
return `${originalRelative}${suffix}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Write the cloud-side bytes to the conflict path. Creates parent dirs as
|
|
73
|
+
* needed (the conflict file always lives next to the original, so the
|
|
74
|
+
* parent already exists in the steady-state — but defense-in-depth).
|
|
75
|
+
*/
|
|
76
|
+
export function writeConflictFile(
|
|
77
|
+
hqRoot: string,
|
|
78
|
+
conflictRelative: string,
|
|
79
|
+
contents: Buffer,
|
|
80
|
+
): void {
|
|
81
|
+
const abs = path.join(hqRoot, conflictRelative);
|
|
82
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
83
|
+
fs.writeFileSync(abs, contents);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Stable conflict ID — used to dedupe re-detections of the same conflict.
|
|
88
|
+
* Re-running sync after a conflict but before the user has resolved should
|
|
89
|
+
* NOT pile up duplicate entries. The id is derived from the original path
|
|
90
|
+
* and the detection timestamp; if the same original conflicts twice with
|
|
91
|
+
* the user resolving in between, that's a new id (different timestamp),
|
|
92
|
+
* which is correct.
|
|
93
|
+
*/
|
|
94
|
+
export function buildConflictId(
|
|
95
|
+
originalRelative: string,
|
|
96
|
+
detectedAt: string,
|
|
97
|
+
): string {
|
|
98
|
+
const safeTs = detectedAt.replace(/:/g, "-").replace(/\.\d+/, "");
|
|
99
|
+
const safePath = originalRelative.replace(/[\/\\.]/g, "-");
|
|
100
|
+
return `${safePath}-${safeTs}`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict index — durable record of pending divergences awaiting resolution.
|
|
3
|
+
*
|
|
4
|
+
* Lives at `<hq_root>/.hq-conflicts/index.json` (inside HQ content, NOT in
|
|
5
|
+
* `~/.hq/`). Two reasons it sits in HQ content rather than in the state dir:
|
|
6
|
+
* 1. The `/resolve-conflicts` HQ skill discovers it relative to the user's
|
|
7
|
+
* HQ folder — that's the user's mental anchor for "where my files are."
|
|
8
|
+
* 2. The conflict-side files themselves live in HQ content, so the index
|
|
9
|
+
* and the files it references stay co-located. If the user moves HQ,
|
|
10
|
+
* the index moves with it.
|
|
11
|
+
*
|
|
12
|
+
* Excluded from cross-machine sync via `.hqignore` — each machine resolves
|
|
13
|
+
* its own queue. We never propagate conflict files (they'd just create more
|
|
14
|
+
* conflicts on the other side).
|
|
15
|
+
*
|
|
16
|
+
* Writes are atomic (tmp + rename). The resolution skill mutates this file
|
|
17
|
+
* mid-walk; a torn write would corrupt the only record of pending conflicts
|
|
18
|
+
* and could lose track of files we'd written to disk. Higher stakes than the
|
|
19
|
+
* journal, which the next sync can rebuild.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as crypto from "crypto";
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import * as path from "path";
|
|
25
|
+
import type { ConflictIndex, ConflictIndexEntry } from "../types.js";
|
|
26
|
+
|
|
27
|
+
const CONFLICTS_DIR = ".hq-conflicts";
|
|
28
|
+
const INDEX_FILENAME = "index.json";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Absolute path to the conflict index for a given HQ root.
|
|
32
|
+
*/
|
|
33
|
+
export function getConflictIndexPath(hqRoot: string): string {
|
|
34
|
+
return path.join(hqRoot, CONFLICTS_DIR, INDEX_FILENAME);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the conflict index. Returns an empty index if the file doesn't exist
|
|
39
|
+
* yet (first-conflict-ever case).
|
|
40
|
+
*
|
|
41
|
+
* Throws on corrupt JSON — we deliberately don't auto-repair, since the only
|
|
42
|
+
* record of pending conflicts is too important to silently overwrite. The
|
|
43
|
+
* `/resolve-conflicts` skill surfaces this case to the user with a manual
|
|
44
|
+
* inspection prompt.
|
|
45
|
+
*/
|
|
46
|
+
export function readConflictIndex(hqRoot: string): ConflictIndex {
|
|
47
|
+
const indexPath = getConflictIndexPath(hqRoot);
|
|
48
|
+
if (!fs.existsSync(indexPath)) {
|
|
49
|
+
return { version: 1, conflicts: [] };
|
|
50
|
+
}
|
|
51
|
+
const raw = fs.readFileSync(indexPath, "utf-8");
|
|
52
|
+
const parsed = JSON.parse(raw) as ConflictIndex;
|
|
53
|
+
// Defensive: an empty file or wrong-shape JSON shouldn't crash callers.
|
|
54
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.conflicts)) {
|
|
55
|
+
return { version: 1, conflicts: [] };
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Atomically write the conflict index. Writes to `<index>.tmp.<random>` then
|
|
62
|
+
* renames into place — `rename(2)` is atomic on POSIX, so a crash mid-write
|
|
63
|
+
* leaves either the old file or the new one, never a half-written one.
|
|
64
|
+
*
|
|
65
|
+
* Always sorts conflicts by `detectedAt` ascending before writing — keeps
|
|
66
|
+
* the file diff-friendly across runs and makes "oldest-first walk" the
|
|
67
|
+
* natural read order in the resolution skill.
|
|
68
|
+
*/
|
|
69
|
+
export function writeConflictIndex(
|
|
70
|
+
hqRoot: string,
|
|
71
|
+
index: ConflictIndex,
|
|
72
|
+
): void {
|
|
73
|
+
const indexPath = getConflictIndexPath(hqRoot);
|
|
74
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
75
|
+
|
|
76
|
+
const sorted: ConflictIndex = {
|
|
77
|
+
version: index.version,
|
|
78
|
+
conflicts: [...index.conflicts].sort((a, b) =>
|
|
79
|
+
a.detectedAt.localeCompare(b.detectedAt),
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Random suffix in tmp name avoids collision if two sync runs ever overlap
|
|
84
|
+
// (shouldn't happen — the runner serializes — but cheap insurance).
|
|
85
|
+
const tmpPath = `${indexPath}.tmp.${crypto.randomBytes(6).toString("hex")}`;
|
|
86
|
+
fs.writeFileSync(tmpPath, JSON.stringify(sorted, null, 2));
|
|
87
|
+
fs.renameSync(tmpPath, indexPath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Idempotent append. If an entry with the same `id` already exists (same
|
|
92
|
+
* original path, same detection timestamp), update it in place rather than
|
|
93
|
+
* duplicating. This matters because re-running sync after a conflict but
|
|
94
|
+
* before resolution will re-detect the same divergence — without dedup the
|
|
95
|
+
* index would grow unboundedly.
|
|
96
|
+
*
|
|
97
|
+
* The "update in place" path also covers the case where the cloud advanced
|
|
98
|
+
* again between detections: we want the latest `remoteVersionId` and
|
|
99
|
+
* `remoteHash` so the resolution skill shows the user the *current* cloud
|
|
100
|
+
* state, not stale data from the first detection.
|
|
101
|
+
*/
|
|
102
|
+
export function appendConflictEntry(
|
|
103
|
+
hqRoot: string,
|
|
104
|
+
entry: ConflictIndexEntry,
|
|
105
|
+
): void {
|
|
106
|
+
const index = readConflictIndex(hqRoot);
|
|
107
|
+
const existingIdx = index.conflicts.findIndex((c) => c.id === entry.id);
|
|
108
|
+
if (existingIdx >= 0) {
|
|
109
|
+
index.conflicts[existingIdx] = entry;
|
|
110
|
+
} else {
|
|
111
|
+
index.conflicts.push(entry);
|
|
112
|
+
}
|
|
113
|
+
writeConflictIndex(hqRoot, index);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove an entry by id. Used by the `/resolve-conflicts` skill after the
|
|
118
|
+
* user picks a resolution and the conflict file is cleaned up. No-op if the
|
|
119
|
+
* id isn't present (e.g. user manually removed the file then re-ran the
|
|
120
|
+
* skill — we want that to be a clean exit, not an error).
|
|
121
|
+
*/
|
|
122
|
+
export function removeConflictEntry(hqRoot: string, id: string): void {
|
|
123
|
+
const index = readConflictIndex(hqRoot);
|
|
124
|
+
const filtered = index.conflicts.filter((c) => c.id !== id);
|
|
125
|
+
if (filtered.length === index.conflicts.length) return;
|
|
126
|
+
writeConflictIndex(hqRoot, { version: index.version, conflicts: filtered });
|
|
127
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the pure conflict primitives — path building, machine-id
|
|
3
|
+
* fallback, atomic index writes, dedup. Kept in one file so the related
|
|
4
|
+
* helpers stay co-located.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import {
|
|
12
|
+
buildConflictPath,
|
|
13
|
+
buildConflictId,
|
|
14
|
+
readShortMachineId,
|
|
15
|
+
} from "./conflict-file.js";
|
|
16
|
+
import {
|
|
17
|
+
appendConflictEntry,
|
|
18
|
+
getConflictIndexPath,
|
|
19
|
+
readConflictIndex,
|
|
20
|
+
removeConflictEntry,
|
|
21
|
+
writeConflictIndex,
|
|
22
|
+
} from "./conflict-index.js";
|
|
23
|
+
import type { ConflictIndexEntry } from "../types.js";
|
|
24
|
+
|
|
25
|
+
describe("buildConflictPath", () => {
|
|
26
|
+
it("inserts the conflict marker before the original extension", () => {
|
|
27
|
+
expect(
|
|
28
|
+
buildConflictPath("knowledge/notes.md", "2026-04-27T22:05:14Z", "abc123"),
|
|
29
|
+
).toBe("knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("preserves nested paths and json extensions", () => {
|
|
33
|
+
expect(
|
|
34
|
+
buildConflictPath("projects/foo/prd.json", "2026-04-27T22:05:14.123Z", "abc123"),
|
|
35
|
+
).toBe("projects/foo/prd.json.conflict-2026-04-27T22-05-14Z-abc123.json");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("appends the suffix verbatim for files without an extension", () => {
|
|
39
|
+
expect(
|
|
40
|
+
buildConflictPath("secrets", "2026-04-27T22:05:14Z", "abc123"),
|
|
41
|
+
).toBe("secrets.conflict-2026-04-27T22-05-14Z-abc123");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("buildConflictId", () => {
|
|
46
|
+
it("escapes path separators and dots so the id is filesystem-safe", () => {
|
|
47
|
+
expect(
|
|
48
|
+
buildConflictId("knowledge/notes.md", "2026-04-27T22:05:14Z"),
|
|
49
|
+
).toBe("knowledge-notes-md-2026-04-27T22-05-14Z");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("yields the same id for the same (path, ts) pair — dedup primitive", () => {
|
|
53
|
+
const a = buildConflictId("foo/bar.md", "2026-04-27T22:05:14Z");
|
|
54
|
+
const b = buildConflictId("foo/bar.md", "2026-04-27T22:05:14Z");
|
|
55
|
+
expect(a).toBe(b);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("readShortMachineId", () => {
|
|
60
|
+
let originalHome: string | undefined;
|
|
61
|
+
let tmpHome: string;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
originalHome = process.env.HOME;
|
|
65
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "hq-machineid-"));
|
|
66
|
+
process.env.HOME = tmpHome;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
if (originalHome) process.env.HOME = originalHome;
|
|
71
|
+
else delete process.env.HOME;
|
|
72
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns the first 6 chars when menubar.json has a machineId", () => {
|
|
76
|
+
fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
path.join(tmpHome, ".hq", "menubar.json"),
|
|
79
|
+
JSON.stringify({ machineId: "deadbeefcafe1234567890" }),
|
|
80
|
+
);
|
|
81
|
+
expect(readShortMachineId()).toBe("deadbe");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("falls back to 'unknown' when menubar.json is missing", () => {
|
|
85
|
+
expect(readShortMachineId()).toBe("unknown");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("falls back to 'unknown' when menubar.json is malformed", () => {
|
|
89
|
+
fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
|
|
90
|
+
fs.writeFileSync(path.join(tmpHome, ".hq", "menubar.json"), "{not-json");
|
|
91
|
+
expect(readShortMachineId()).toBe("unknown");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("conflict index", () => {
|
|
96
|
+
let tmpHq: string;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
tmpHq = fs.mkdtempSync(path.join(os.tmpdir(), "hq-cidx-"));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
fs.rmSync(tmpHq, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function entry(overrides: Partial<ConflictIndexEntry> = {}): ConflictIndexEntry {
|
|
107
|
+
return {
|
|
108
|
+
id: "knowledge-notes-md-2026-04-27T22-05-14Z",
|
|
109
|
+
originalPath: "knowledge/notes.md",
|
|
110
|
+
conflictPath: "knowledge/notes.md.conflict-2026-04-27T22-05-14Z-abc123.md",
|
|
111
|
+
detectedAt: "2026-04-27T22:05:14Z",
|
|
112
|
+
side: "pull",
|
|
113
|
+
machineId: "abc123",
|
|
114
|
+
localHash: "local",
|
|
115
|
+
remoteHash: "remote",
|
|
116
|
+
remoteVersionId: "v2",
|
|
117
|
+
lastKnownVersionId: "v1",
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
it("returns an empty index when the file does not exist", () => {
|
|
123
|
+
const idx = readConflictIndex(tmpHq);
|
|
124
|
+
expect(idx).toEqual({ version: 1, conflicts: [] });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("creates the .hq-conflicts dir on first write", () => {
|
|
128
|
+
appendConflictEntry(tmpHq, entry());
|
|
129
|
+
expect(fs.existsSync(getConflictIndexPath(tmpHq))).toBe(true);
|
|
130
|
+
expect(fs.existsSync(path.join(tmpHq, ".hq-conflicts"))).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("appends new entries and dedupes on id (idempotent re-detection)", () => {
|
|
134
|
+
appendConflictEntry(tmpHq, entry({ id: "a", remoteVersionId: "v1" }));
|
|
135
|
+
appendConflictEntry(tmpHq, entry({ id: "b", remoteVersionId: "v1" }));
|
|
136
|
+
// Re-detect "a" — the second push should update in place, not duplicate.
|
|
137
|
+
appendConflictEntry(tmpHq, entry({ id: "a", remoteVersionId: "v9" }));
|
|
138
|
+
|
|
139
|
+
const idx = readConflictIndex(tmpHq);
|
|
140
|
+
expect(idx.conflicts).toHaveLength(2);
|
|
141
|
+
const a = idx.conflicts.find((c) => c.id === "a");
|
|
142
|
+
expect(a?.remoteVersionId).toBe("v9"); // updated, not appended
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("sorts conflicts by detectedAt ascending on every write", () => {
|
|
146
|
+
appendConflictEntry(tmpHq, entry({ id: "newer", detectedAt: "2026-04-27T23:00:00Z" }));
|
|
147
|
+
appendConflictEntry(tmpHq, entry({ id: "older", detectedAt: "2026-04-27T22:00:00Z" }));
|
|
148
|
+
const idx = readConflictIndex(tmpHq);
|
|
149
|
+
expect(idx.conflicts.map((c) => c.id)).toEqual(["older", "newer"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("removeConflictEntry removes a single entry by id", () => {
|
|
153
|
+
appendConflictEntry(tmpHq, entry({ id: "keep" }));
|
|
154
|
+
appendConflictEntry(tmpHq, entry({ id: "drop" }));
|
|
155
|
+
removeConflictEntry(tmpHq, "drop");
|
|
156
|
+
const idx = readConflictIndex(tmpHq);
|
|
157
|
+
expect(idx.conflicts.map((c) => c.id)).toEqual(["keep"]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("removeConflictEntry is a no-op when the id is not present", () => {
|
|
161
|
+
appendConflictEntry(tmpHq, entry({ id: "a" }));
|
|
162
|
+
expect(() => removeConflictEntry(tmpHq, "missing")).not.toThrow();
|
|
163
|
+
expect(readConflictIndex(tmpHq).conflicts).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("writeConflictIndex leaves no .tmp files on disk after success", () => {
|
|
167
|
+
writeConflictIndex(tmpHq, { version: 1, conflicts: [entry()] });
|
|
168
|
+
const files = fs.readdirSync(path.join(tmpHq, ".hq-conflicts"));
|
|
169
|
+
// Only index.json should remain; tmp files renamed atomically into place.
|
|
170
|
+
expect(files).toEqual(["index.json"]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns an empty index for malformed-but-parseable JSON", () => {
|
|
174
|
+
const indexPath = getConflictIndexPath(tmpHq);
|
|
175
|
+
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
176
|
+
fs.writeFileSync(indexPath, JSON.stringify({ version: 1 })); // no `conflicts` array
|
|
177
|
+
const idx = readConflictIndex(tmpHq);
|
|
178
|
+
expect(idx.conflicts).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -102,3 +102,30 @@ export interface VaultServiceConfig {
|
|
|
102
102
|
/** AWS region for S3 client (defaults to entity region or us-east-1) */
|
|
103
103
|
region?: string;
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
// ── Conflict index (consumed by /resolve-conflicts) ─────────────────────────
|
|
107
|
+
//
|
|
108
|
+
// Restored from feat/lineage-conflict-tracking (83bf5fa1) so the producer
|
|
109
|
+
// can write `.conflict-<ts>-<machine>.<ext>` mirror files + append to
|
|
110
|
+
// `<hqRoot>/.hq-conflicts/index.json` whenever the hash-comparison detector
|
|
111
|
+
// flags a conflict that doesn't get overwritten or aborted.
|
|
112
|
+
|
|
113
|
+
export interface ConflictIndexEntry {
|
|
114
|
+
id: string;
|
|
115
|
+
originalPath: string;
|
|
116
|
+
conflictPath: string;
|
|
117
|
+
detectedAt: string;
|
|
118
|
+
side: "push" | "pull";
|
|
119
|
+
machineId: string;
|
|
120
|
+
localHash: string;
|
|
121
|
+
remoteHash: string;
|
|
122
|
+
/** S3 VersionId when known (present for VersionId-aware buckets). */
|
|
123
|
+
remoteVersionId?: string;
|
|
124
|
+
/** Last-known parent VersionId from journal, when known. */
|
|
125
|
+
lastKnownVersionId?: string | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ConflictIndex {
|
|
129
|
+
version: 1;
|
|
130
|
+
conflicts: ConflictIndexEntry[];
|
|
131
|
+
}
|