@indigoai-us/hq-cloud 5.4.6 → 5.7.1
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/bin/sync-runner.d.ts +13 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +37 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +82 -16
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +102 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +22 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +187 -62
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +81 -0
- package/dist/cli/sync.test.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/bin/sync-runner.test.ts +43 -0
- package/src/bin/sync-runner.ts +25 -2
- package/src/cli/share.test.ts +125 -0
- package/src/cli/share.ts +133 -18
- package/src/cli/sync.test.ts +97 -0
- package/src/cli/sync.ts +277 -68
- 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
|
@@ -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
|
@@ -575,6 +575,49 @@ describe("per-company fanout", () => {
|
|
|
575
575
|
]);
|
|
576
576
|
});
|
|
577
577
|
|
|
578
|
+
it("tags Stage-1 plan events with the company slug", async () => {
|
|
579
|
+
const deps = makeDeps({
|
|
580
|
+
createVaultClient: () =>
|
|
581
|
+
makeVaultStub({
|
|
582
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
583
|
+
entityGet: (uid: string) =>
|
|
584
|
+
Promise.resolve({ uid, slug: "acme" } as unknown as EntityInfo),
|
|
585
|
+
}),
|
|
586
|
+
sync: vi.fn().mockImplementation(async (opts: SyncOptions) => {
|
|
587
|
+
opts.onEvent?.({
|
|
588
|
+
type: "plan",
|
|
589
|
+
filesToDownload: 7,
|
|
590
|
+
bytesToDownload: 4096,
|
|
591
|
+
filesToUpload: 0,
|
|
592
|
+
bytesToUpload: 0,
|
|
593
|
+
filesToSkip: 3,
|
|
594
|
+
filesToConflict: 1,
|
|
595
|
+
});
|
|
596
|
+
return defaultSyncResult({ filesDownloaded: 7, bytesDownloaded: 4096 });
|
|
597
|
+
}),
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const code = await runRunner(["--companies"], deps);
|
|
601
|
+
expect(code).toBe(0);
|
|
602
|
+
const planEvents = deps.stdout
|
|
603
|
+
.events()
|
|
604
|
+
.filter((e): e is Extract<RunnerEvent, { type: "plan" }> =>
|
|
605
|
+
e.type === "plan",
|
|
606
|
+
);
|
|
607
|
+
expect(planEvents).toEqual([
|
|
608
|
+
{
|
|
609
|
+
type: "plan",
|
|
610
|
+
company: "acme",
|
|
611
|
+
filesToDownload: 7,
|
|
612
|
+
bytesToDownload: 4096,
|
|
613
|
+
filesToUpload: 0,
|
|
614
|
+
bytesToUpload: 0,
|
|
615
|
+
filesToSkip: 3,
|
|
616
|
+
filesToConflict: 1,
|
|
617
|
+
},
|
|
618
|
+
]);
|
|
619
|
+
});
|
|
620
|
+
|
|
578
621
|
it("tags per-file error events with the company slug", async () => {
|
|
579
622
|
const deps = makeDeps({
|
|
580
623
|
createVaultClient: () =>
|
package/src/bin/sync-runner.ts
CHANGED
|
@@ -125,6 +125,18 @@ export type RunnerEvent =
|
|
|
125
125
|
type: "fanout-plan";
|
|
126
126
|
companies: Array<{ uid: string; slug: string; name?: string }>;
|
|
127
127
|
}
|
|
128
|
+
| ({
|
|
129
|
+
/**
|
|
130
|
+
* Stage-1 results for a single company's sync/share pass. Emitted once
|
|
131
|
+
* before any `progress` events for that company arrive — once for the
|
|
132
|
+
* pull phase (download counts) and once for the push phase (upload
|
|
133
|
+
* counts) when `--direction both`. Consumers (the menubar) sum the
|
|
134
|
+
* non-zero fields across all `plan` events seen for a fanout to render
|
|
135
|
+
* an accurate "X of Y files" denominator before transfers begin.
|
|
136
|
+
*/
|
|
137
|
+
type: "plan";
|
|
138
|
+
company: string;
|
|
139
|
+
} & Omit<Extract<SyncProgressEvent, { type: "plan" }>, "type">)
|
|
128
140
|
| ({ type: "progress"; company: string } & Omit<Extract<SyncProgressEvent, { type: "progress" }>, "type">)
|
|
129
141
|
| ({ type: "error"; company?: string } & Omit<Extract<SyncProgressEvent, { type: "error" }>, "type">)
|
|
130
142
|
| ({ type: "conflict"; company: string } & Omit<Extract<SyncProgressEvent, { type: "conflict" }>, "type">)
|
|
@@ -534,7 +546,18 @@ export async function runRunner(
|
|
|
534
546
|
// Per-company event tagger — shared by push and pull phases so progress
|
|
535
547
|
// rows land on the right company regardless of which phase emitted them.
|
|
536
548
|
const tagAndEmit = (event: SyncProgressEvent): void => {
|
|
537
|
-
if (event.type === "
|
|
549
|
+
if (event.type === "plan") {
|
|
550
|
+
emit({
|
|
551
|
+
type: "plan",
|
|
552
|
+
company: companyLabel,
|
|
553
|
+
filesToDownload: event.filesToDownload,
|
|
554
|
+
bytesToDownload: event.bytesToDownload,
|
|
555
|
+
filesToUpload: event.filesToUpload,
|
|
556
|
+
bytesToUpload: event.bytesToUpload,
|
|
557
|
+
filesToSkip: event.filesToSkip,
|
|
558
|
+
filesToConflict: event.filesToConflict,
|
|
559
|
+
});
|
|
560
|
+
} else if (event.type === "progress") {
|
|
538
561
|
emit({
|
|
539
562
|
type: "progress",
|
|
540
563
|
company: companyLabel,
|
|
@@ -550,7 +573,7 @@ export async function runRunner(
|
|
|
550
573
|
direction: event.direction,
|
|
551
574
|
resolution: event.resolution,
|
|
552
575
|
});
|
|
553
|
-
} else {
|
|
576
|
+
} else if (event.type === "error") {
|
|
554
577
|
emit({
|
|
555
578
|
type: "error",
|
|
556
579
|
company: companyLabel,
|
package/src/cli/share.test.ts
CHANGED
|
@@ -508,6 +508,9 @@ describe("share", () => {
|
|
|
508
508
|
vaultConfig: mockConfig,
|
|
509
509
|
hqRoot: tmpDir,
|
|
510
510
|
onEvent: (e) => {
|
|
511
|
+
// Only file-level events carry `.path`. The Stage-1 `plan` event is
|
|
512
|
+
// surfaced separately and tested in its own block.
|
|
513
|
+
if (e.type === "plan") return;
|
|
511
514
|
events.push({
|
|
512
515
|
type: e.type,
|
|
513
516
|
path: e.path,
|
|
@@ -521,4 +524,126 @@ describe("share", () => {
|
|
|
521
524
|
expect(events.every((e) => e.type === "progress")).toBe(true);
|
|
522
525
|
expect(events.map((e) => e.path).sort()).toEqual(["a.md", "b.md"]);
|
|
523
526
|
});
|
|
527
|
+
|
|
528
|
+
// ── Stage-1 plan event ─────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
it("emits a plan event before any progress events", async () => {
|
|
531
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
532
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
533
|
+
fs.writeFileSync(path.join(companyRoot, "a.md"), "alpha");
|
|
534
|
+
fs.writeFileSync(path.join(companyRoot, "b.md"), "beta");
|
|
535
|
+
|
|
536
|
+
const events: { type: string }[] = [];
|
|
537
|
+
await share({
|
|
538
|
+
paths: [companyRoot],
|
|
539
|
+
company: "acme",
|
|
540
|
+
vaultConfig: mockConfig,
|
|
541
|
+
hqRoot: tmpDir,
|
|
542
|
+
onEvent: (e) => events.push({ type: e.type }),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(events.length).toBeGreaterThan(0);
|
|
546
|
+
expect(events[0].type).toBe("plan");
|
|
547
|
+
const planIndex = events.findIndex((e) => e.type === "plan");
|
|
548
|
+
const firstProgressIndex = events.findIndex((e) => e.type === "progress");
|
|
549
|
+
expect(firstProgressIndex).toBeGreaterThan(planIndex);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("plan event reports filesToUpload = candidates and bytesToUpload = sum of file sizes", async () => {
|
|
553
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
554
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
555
|
+
fs.writeFileSync(path.join(companyRoot, "a.md"), "alpha"); // 5 bytes
|
|
556
|
+
fs.writeFileSync(path.join(companyRoot, "b.md"), "beta!"); // 5 bytes
|
|
557
|
+
|
|
558
|
+
const planEvents: Array<{
|
|
559
|
+
type: string;
|
|
560
|
+
filesToUpload?: number;
|
|
561
|
+
bytesToUpload?: number;
|
|
562
|
+
filesToDownload?: number;
|
|
563
|
+
bytesToDownload?: number;
|
|
564
|
+
filesToSkip?: number;
|
|
565
|
+
filesToConflict?: number;
|
|
566
|
+
}> = [];
|
|
567
|
+
await share({
|
|
568
|
+
paths: [companyRoot],
|
|
569
|
+
company: "acme",
|
|
570
|
+
vaultConfig: mockConfig,
|
|
571
|
+
hqRoot: tmpDir,
|
|
572
|
+
onEvent: (e) => {
|
|
573
|
+
if (e.type === "plan") planEvents.push(e);
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
expect(planEvents).toHaveLength(1);
|
|
578
|
+
expect(planEvents[0]).toMatchObject({
|
|
579
|
+
type: "plan",
|
|
580
|
+
filesToUpload: 2,
|
|
581
|
+
bytesToUpload: 10,
|
|
582
|
+
filesToDownload: 0, // share() is push-only
|
|
583
|
+
bytesToDownload: 0,
|
|
584
|
+
filesToSkip: 0,
|
|
585
|
+
// Push conflicts can't be classified pre-HEAD (V1 limitation);
|
|
586
|
+
// the complete event reports the authoritative count.
|
|
587
|
+
filesToConflict: 0,
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("plan event filesToSkip reflects skip-unchanged hits when journal hash matches", async () => {
|
|
592
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
593
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
594
|
+
fs.writeFileSync(path.join(companyRoot, "unchanged.md"), "stable content");
|
|
595
|
+
fs.writeFileSync(path.join(companyRoot, "changed.md"), "newer content");
|
|
596
|
+
|
|
597
|
+
// Pre-seed the journal so unchanged.md matches its hash but
|
|
598
|
+
// changed.md does not.
|
|
599
|
+
const crypto = await import("crypto");
|
|
600
|
+
const unchangedHash = crypto
|
|
601
|
+
.createHash("sha256")
|
|
602
|
+
.update("stable content")
|
|
603
|
+
.digest("hex");
|
|
604
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
605
|
+
fs.writeFileSync(
|
|
606
|
+
journalPath,
|
|
607
|
+
JSON.stringify({
|
|
608
|
+
version: "1",
|
|
609
|
+
lastSync: new Date().toISOString(),
|
|
610
|
+
files: {
|
|
611
|
+
"unchanged.md": {
|
|
612
|
+
hash: unchangedHash,
|
|
613
|
+
size: 14,
|
|
614
|
+
syncedAt: new Date().toISOString(),
|
|
615
|
+
direction: "up",
|
|
616
|
+
},
|
|
617
|
+
"changed.md": {
|
|
618
|
+
hash: "stale-hash",
|
|
619
|
+
size: 13,
|
|
620
|
+
syncedAt: new Date().toISOString(),
|
|
621
|
+
direction: "up",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
}),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const planEvents: Array<{
|
|
628
|
+
type: string;
|
|
629
|
+
filesToUpload?: number;
|
|
630
|
+
filesToSkip?: number;
|
|
631
|
+
}> = [];
|
|
632
|
+
await share({
|
|
633
|
+
paths: [companyRoot],
|
|
634
|
+
company: "acme",
|
|
635
|
+
vaultConfig: mockConfig,
|
|
636
|
+
hqRoot: tmpDir,
|
|
637
|
+
skipUnchanged: true,
|
|
638
|
+
onEvent: (e) => {
|
|
639
|
+
if (e.type === "plan") planEvents.push(e);
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
expect(planEvents).toHaveLength(1);
|
|
644
|
+
expect(planEvents[0]).toMatchObject({
|
|
645
|
+
filesToUpload: 1,
|
|
646
|
+
filesToSkip: 1,
|
|
647
|
+
});
|
|
648
|
+
});
|
|
524
649
|
});
|
package/src/cli/share.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
|
-
import type { VaultServiceConfig } from "../types.js";
|
|
10
|
+
import type { VaultServiceConfig, SyncJournal } from "../types.js";
|
|
11
11
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
12
12
|
import { uploadFile, headRemoteFile } from "../s3.js";
|
|
13
13
|
import { readJournal, writeJournal, hashFile, updateEntry, normalizeEtag } from "../journal.js";
|
|
@@ -16,6 +16,100 @@ import { resolveConflict } from "./conflict.js";
|
|
|
16
16
|
import type { ConflictStrategy } from "./conflict.js";
|
|
17
17
|
import type { SyncProgressEvent } from "./sync.js";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Stage-1 classification for a single local file in a push run. Pre-HEAD —
|
|
21
|
+
* only inputs we can evaluate locally (size limit, journal hash, optional
|
|
22
|
+
* skip-unchanged) determine the action. Files that pass classification as
|
|
23
|
+
* `upload` are still subject to a per-file HEAD + 3-way conflict check in
|
|
24
|
+
* Stage 2 before the actual PUT, so the `filesToUpload` count in the plan
|
|
25
|
+
* event is an upper bound: it includes files that may turn out to be
|
|
26
|
+
* conflicts. V1.5 follow-up: replace per-file HEAD with a single LIST so
|
|
27
|
+
* conflicts can be classified up-front and reported in the plan.
|
|
28
|
+
*/
|
|
29
|
+
type PushPlanItem =
|
|
30
|
+
| {
|
|
31
|
+
action: "upload";
|
|
32
|
+
absolutePath: string;
|
|
33
|
+
relativePath: string;
|
|
34
|
+
localHash: string;
|
|
35
|
+
size: number;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
action: "skip-size-limit";
|
|
39
|
+
absolutePath: string;
|
|
40
|
+
relativePath: string;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
action: "skip-unchanged";
|
|
44
|
+
absolutePath: string;
|
|
45
|
+
relativePath: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
interface PushPlan {
|
|
49
|
+
items: PushPlanItem[];
|
|
50
|
+
filesToUpload: number;
|
|
51
|
+
bytesToUpload: number;
|
|
52
|
+
filesToSkip: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pure Stage-1 pass for push: walk the candidate file list, hash each one,
|
|
57
|
+
* apply the size-limit and skip-unchanged gates, and return a classified
|
|
58
|
+
* plan plus aggregate counts. No S3 calls, no journal writes, no event
|
|
59
|
+
* emission.
|
|
60
|
+
*
|
|
61
|
+
* The conflict count is intentionally absent from the returned `PushPlan` —
|
|
62
|
+
* detecting a push conflict requires a remote HEAD that we defer to Stage 2.
|
|
63
|
+
* Consumers that want a conflict count get it from the `complete` event.
|
|
64
|
+
*/
|
|
65
|
+
function computePushPlan(
|
|
66
|
+
filesToShare: { absolutePath: string; relativePath: string }[],
|
|
67
|
+
journal: SyncJournal,
|
|
68
|
+
skipUnchanged: boolean,
|
|
69
|
+
): PushPlan {
|
|
70
|
+
const items: PushPlanItem[] = [];
|
|
71
|
+
|
|
72
|
+
for (const { absolutePath, relativePath } of filesToShare) {
|
|
73
|
+
if (!isWithinSizeLimit(absolutePath)) {
|
|
74
|
+
items.push({ action: "skip-size-limit", absolutePath, relativePath });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const localHash = hashFile(absolutePath);
|
|
79
|
+
|
|
80
|
+
if (skipUnchanged) {
|
|
81
|
+
const existing = journal.files[relativePath];
|
|
82
|
+
if (existing && existing.hash === localHash) {
|
|
83
|
+
items.push({ action: "skip-unchanged", absolutePath, relativePath });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const size = fs.statSync(absolutePath).size;
|
|
89
|
+
items.push({
|
|
90
|
+
action: "upload",
|
|
91
|
+
absolutePath,
|
|
92
|
+
relativePath,
|
|
93
|
+
localHash,
|
|
94
|
+
size,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let filesToUpload = 0;
|
|
99
|
+
let bytesToUpload = 0;
|
|
100
|
+
let filesToSkip = 0;
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
if (item.action === "upload") {
|
|
103
|
+
filesToUpload++;
|
|
104
|
+
bytesToUpload += item.size;
|
|
105
|
+
} else {
|
|
106
|
+
filesToSkip++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { items, filesToUpload, bytesToUpload, filesToSkip };
|
|
111
|
+
}
|
|
112
|
+
|
|
19
113
|
export interface ShareOptions {
|
|
20
114
|
/** Path(s) to share (files or directories) */
|
|
21
115
|
paths: string[];
|
|
@@ -94,30 +188,45 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
|
|
|
94
188
|
// Collect all files to share
|
|
95
189
|
const filesToShare = collectFiles(paths, hqRoot, syncRoot, shouldSync);
|
|
96
190
|
|
|
97
|
-
|
|
98
|
-
|
|
191
|
+
// Stage 1: classify each file. Pre-HEAD — only inputs we can evaluate
|
|
192
|
+
// locally (size limit, journal hash, optional skip-unchanged) are
|
|
193
|
+
// considered. The plan event below carries an upper-bound `filesToUpload`
|
|
194
|
+
// (true conflicts emerge from the per-file HEAD in Stage 2 and aren't
|
|
195
|
+
// knowable here). The final `complete` event reports authoritative counts.
|
|
196
|
+
const plan = computePushPlan(filesToShare, journal, skipUnchanged === true);
|
|
197
|
+
|
|
198
|
+
emit({
|
|
199
|
+
type: "plan",
|
|
200
|
+
// share() is push-only; pull counts are sourced from sync()'s plan event.
|
|
201
|
+
filesToDownload: 0,
|
|
202
|
+
bytesToDownload: 0,
|
|
203
|
+
filesToUpload: plan.filesToUpload,
|
|
204
|
+
bytesToUpload: plan.bytesToUpload,
|
|
205
|
+
filesToSkip: plan.filesToSkip,
|
|
206
|
+
// Push conflicts require a remote HEAD; we don't yet do that in Stage 1,
|
|
207
|
+
// so this stays 0. V1.5 (single LIST) will let us classify them up-front.
|
|
208
|
+
filesToConflict: 0,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Stage 2: execute. Skip items pre-classified as no-ops, then for each
|
|
212
|
+
// upload candidate run the HEAD + 3-way conflict check + actual PUT.
|
|
213
|
+
for (const item of plan.items) {
|
|
214
|
+
if (item.action === "skip-size-limit") {
|
|
99
215
|
emit({
|
|
100
216
|
type: "error",
|
|
101
|
-
path: relativePath,
|
|
217
|
+
path: item.relativePath,
|
|
102
218
|
message: "file exceeds size limit",
|
|
103
219
|
});
|
|
104
220
|
filesSkipped++;
|
|
105
221
|
continue;
|
|
106
222
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// every file every tick. Off by default so `hq share <file>` keeps its
|
|
111
|
-
// explicit-intent semantics (user named it, user wants it sent).
|
|
112
|
-
const localHash = hashFile(absolutePath);
|
|
113
|
-
if (skipUnchanged) {
|
|
114
|
-
const existing = journal.files[relativePath];
|
|
115
|
-
if (existing && existing.hash === localHash) {
|
|
116
|
-
filesSkipped++;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
223
|
+
if (item.action === "skip-unchanged") {
|
|
224
|
+
filesSkipped++;
|
|
225
|
+
continue;
|
|
119
226
|
}
|
|
120
227
|
|
|
228
|
+
const { absolutePath, relativePath, localHash } = item;
|
|
229
|
+
|
|
121
230
|
// Auto-refresh context if credentials expiring
|
|
122
231
|
if (isExpiringSoon(ctx.expiresAt)) {
|
|
123
232
|
ctx = await refreshEntityContext(companyRef, vaultConfig);
|
|
@@ -225,7 +334,13 @@ export async function share(options: ShareOptions): Promise<ShareResult> {
|
|
|
225
334
|
* emitted before `onEvent` was added — tty users see no change.
|
|
226
335
|
*/
|
|
227
336
|
function defaultConsoleLogger(event: SyncProgressEvent): void {
|
|
228
|
-
if (event.type === "
|
|
337
|
+
if (event.type === "plan") {
|
|
338
|
+
if (event.filesToUpload > 0) {
|
|
339
|
+
console.log(
|
|
340
|
+
`Plan: ${event.filesToUpload} to upload (${event.bytesToUpload} bytes), ${event.filesToSkip} unchanged`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
} else if (event.type === "progress") {
|
|
229
344
|
if (event.message) {
|
|
230
345
|
console.log(` ✓ ${event.path} — "${event.message}"`);
|
|
231
346
|
} else {
|
|
@@ -235,7 +350,7 @@ function defaultConsoleLogger(event: SyncProgressEvent): void {
|
|
|
235
350
|
console.error(
|
|
236
351
|
` ⚠ conflict (${event.direction}): ${event.path} — ${event.resolution}`,
|
|
237
352
|
);
|
|
238
|
-
} else {
|
|
353
|
+
} else if (event.type === "error") {
|
|
239
354
|
console.error(` ✗ ${event.path} — ${event.message}`);
|
|
240
355
|
}
|
|
241
356
|
}
|
package/src/cli/sync.test.ts
CHANGED
|
@@ -384,4 +384,101 @@ describe("sync", () => {
|
|
|
384
384
|
expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
|
|
385
385
|
expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
|
|
386
386
|
});
|
|
387
|
+
|
|
388
|
+
// ── Stage-1 plan event ─────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
it("emits a plan event before any progress events", async () => {
|
|
391
|
+
const events: { type: string }[] = [];
|
|
392
|
+
await sync({
|
|
393
|
+
company: "acme",
|
|
394
|
+
vaultConfig: mockConfig,
|
|
395
|
+
hqRoot: tmpDir,
|
|
396
|
+
onEvent: (e) => events.push({ type: e.type }),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Plan must be the first event so consumers can use its totals as
|
|
400
|
+
// the progress denominator before any per-file events arrive.
|
|
401
|
+
expect(events.length).toBeGreaterThan(0);
|
|
402
|
+
expect(events[0].type).toBe("plan");
|
|
403
|
+
const planIndex = events.findIndex((e) => e.type === "plan");
|
|
404
|
+
const firstProgressIndex = events.findIndex((e) => e.type === "progress");
|
|
405
|
+
expect(firstProgressIndex).toBeGreaterThan(planIndex);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("plan event totals reflect the upcoming Stage-2 work (all-new case)", async () => {
|
|
409
|
+
// Both mock remote files are new locally → both counted as downloads,
|
|
410
|
+
// bytes summed from listRemoteFiles, no conflicts, no skips.
|
|
411
|
+
const planEvents: unknown[] = [];
|
|
412
|
+
await sync({
|
|
413
|
+
company: "acme",
|
|
414
|
+
vaultConfig: mockConfig,
|
|
415
|
+
hqRoot: tmpDir,
|
|
416
|
+
onEvent: (e) => {
|
|
417
|
+
if (e.type === "plan") {
|
|
418
|
+
planEvents.push(e);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
expect(planEvents).toHaveLength(1);
|
|
424
|
+
expect(planEvents[0]).toMatchObject({
|
|
425
|
+
type: "plan",
|
|
426
|
+
filesToDownload: 2,
|
|
427
|
+
bytesToDownload: 142, // 42 + 100 from the s3 mock
|
|
428
|
+
filesToUpload: 0, // sync() never plans uploads
|
|
429
|
+
bytesToUpload: 0,
|
|
430
|
+
filesToSkip: 0,
|
|
431
|
+
filesToConflict: 0,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("plan event counts a 3-way conflict separately from downloads", async () => {
|
|
436
|
+
// Local edit + journal-tracked + remote ETag drifted → conflict.
|
|
437
|
+
const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
|
|
438
|
+
fs.mkdirSync(companyDocs, { recursive: true });
|
|
439
|
+
fs.writeFileSync(path.join(companyDocs, "handoff.md"), "local edit");
|
|
440
|
+
|
|
441
|
+
fs.writeFileSync(
|
|
442
|
+
journalPath,
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
version: "1",
|
|
445
|
+
lastSync: new Date().toISOString(),
|
|
446
|
+
files: {
|
|
447
|
+
"docs/handoff.md": {
|
|
448
|
+
hash: "stale-hash-from-pre-edit",
|
|
449
|
+
size: 20,
|
|
450
|
+
syncedAt: new Date(Date.now() - 3600000).toISOString(),
|
|
451
|
+
direction: "down",
|
|
452
|
+
// Mismatched ETag — listRemoteFiles mock returns "abc123",
|
|
453
|
+
// we record a stale one so remoteChanged is true.
|
|
454
|
+
remoteEtag: "stale-remote-etag",
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const planEvents: Array<{
|
|
461
|
+
type: string;
|
|
462
|
+
filesToDownload?: number;
|
|
463
|
+
filesToConflict?: number;
|
|
464
|
+
filesToSkip?: number;
|
|
465
|
+
}> = [];
|
|
466
|
+
await sync({
|
|
467
|
+
company: "acme",
|
|
468
|
+
onConflict: "keep",
|
|
469
|
+
vaultConfig: mockConfig,
|
|
470
|
+
hqRoot: tmpDir,
|
|
471
|
+
onEvent: (e) => {
|
|
472
|
+
if (e.type === "plan") planEvents.push(e);
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(planEvents).toHaveLength(1);
|
|
477
|
+
// Conflict is counted separately; only the new file is in toDownload.
|
|
478
|
+
expect(planEvents[0]).toMatchObject({
|
|
479
|
+
filesToDownload: 1,
|
|
480
|
+
filesToConflict: 1,
|
|
481
|
+
filesToSkip: 0,
|
|
482
|
+
});
|
|
483
|
+
});
|
|
387
484
|
});
|