ccsini 0.1.60 → 0.1.62
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/index.js +195 -93
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27876,7 +27876,7 @@ function decodeProjectPath(encoded) {
|
|
|
27876
27876
|
}
|
|
27877
27877
|
|
|
27878
27878
|
// ../shared/src/constants.ts
|
|
27879
|
-
var DEFAULT_CATEGORY_PATTERNS, MERGE_STRATEGIES, NEVER_SYNC_PATTERNS, SESSION_SYNC_DEFAULTS, JWT_EXPIRY_SECONDS = 900, DAEMON_DEBOUNCE_MS = 5000, DAEMON_INTERVAL_MS = 30000, MAX_CONCURRENT_TRANSFERS = 50;
|
|
27879
|
+
var DEFAULT_CATEGORY_PATTERNS, MERGE_STRATEGIES, NEVER_SYNC_PATTERNS, SESSION_SYNC_DEFAULTS, JWT_EXPIRY_SECONDS = 900, DAEMON_DEBOUNCE_MS = 5000, DAEMON_INTERVAL_MS = 30000, MAX_CONCURRENT_TRANSFERS = 50, BATCH_DOWNLOAD_SIZE = 20;
|
|
27880
27880
|
var init_constants = __esm(() => {
|
|
27881
27881
|
DEFAULT_CATEGORY_PATTERNS = {
|
|
27882
27882
|
session: ["projects/**/*.jsonl"],
|
|
@@ -28058,7 +28058,7 @@ var {
|
|
|
28058
28058
|
} = import__.default;
|
|
28059
28059
|
|
|
28060
28060
|
// src/version.ts
|
|
28061
|
-
var VERSION = "0.1.
|
|
28061
|
+
var VERSION = "0.1.62";
|
|
28062
28062
|
|
|
28063
28063
|
// src/commands/init.ts
|
|
28064
28064
|
init_source();
|
|
@@ -28967,6 +28967,23 @@ class CcsiniClient {
|
|
|
28967
28967
|
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
28968
28968
|
this.token = token;
|
|
28969
28969
|
}
|
|
28970
|
+
async fetchWithRetry(input, init, maxRetries = 3) {
|
|
28971
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
28972
|
+
try {
|
|
28973
|
+
const res = await fetch(input, init);
|
|
28974
|
+
if (res.status >= 500 && attempt < maxRetries) {
|
|
28975
|
+
await new Promise((r) => setTimeout(r, 500 * Math.pow(2, attempt - 1)));
|
|
28976
|
+
continue;
|
|
28977
|
+
}
|
|
28978
|
+
return res;
|
|
28979
|
+
} catch (e) {
|
|
28980
|
+
if (attempt >= maxRetries)
|
|
28981
|
+
throw e;
|
|
28982
|
+
await new Promise((r) => setTimeout(r, 500 * Math.pow(2, attempt - 1)));
|
|
28983
|
+
}
|
|
28984
|
+
}
|
|
28985
|
+
throw new Error("fetchWithRetry exhausted");
|
|
28986
|
+
}
|
|
28970
28987
|
getHeaders() {
|
|
28971
28988
|
return {
|
|
28972
28989
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -28995,7 +29012,7 @@ class CcsiniClient {
|
|
|
28995
29012
|
throw new Error("Failed to store salt");
|
|
28996
29013
|
}
|
|
28997
29014
|
async getManifest() {
|
|
28998
|
-
const res = await
|
|
29015
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/manifest`, {
|
|
28999
29016
|
headers: this.getHeaders()
|
|
29000
29017
|
});
|
|
29001
29018
|
if (!res.ok)
|
|
@@ -29006,7 +29023,7 @@ class CcsiniClient {
|
|
|
29006
29023
|
return new Uint8Array(Buffer.from(data.manifest, "base64"));
|
|
29007
29024
|
}
|
|
29008
29025
|
async putManifest(encryptedManifest) {
|
|
29009
|
-
const res = await
|
|
29026
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/manifest`, {
|
|
29010
29027
|
method: "PUT",
|
|
29011
29028
|
headers: {
|
|
29012
29029
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -29018,7 +29035,7 @@ class CcsiniClient {
|
|
|
29018
29035
|
throw new Error("Failed to upload manifest");
|
|
29019
29036
|
}
|
|
29020
29037
|
async uploadBlob(hash, encryptedData) {
|
|
29021
|
-
const res = await
|
|
29038
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blob/${hash}`, {
|
|
29022
29039
|
method: "PUT",
|
|
29023
29040
|
headers: {
|
|
29024
29041
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -29030,7 +29047,7 @@ class CcsiniClient {
|
|
|
29030
29047
|
throw new Error(`Failed to upload blob ${hash}`);
|
|
29031
29048
|
}
|
|
29032
29049
|
async downloadBlob(hash) {
|
|
29033
|
-
const res = await
|
|
29050
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blob/${hash}`, {
|
|
29034
29051
|
headers: this.getHeaders()
|
|
29035
29052
|
});
|
|
29036
29053
|
if (!res.ok)
|
|
@@ -29039,7 +29056,7 @@ class CcsiniClient {
|
|
|
29039
29056
|
return new Uint8Array(buffer);
|
|
29040
29057
|
}
|
|
29041
29058
|
async batchDownload(hashes) {
|
|
29042
|
-
const res = await
|
|
29059
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/batch-download`, {
|
|
29043
29060
|
method: "POST",
|
|
29044
29061
|
headers: this.getHeaders(),
|
|
29045
29062
|
body: JSON.stringify({ hashes })
|
|
@@ -29054,7 +29071,7 @@ class CcsiniClient {
|
|
|
29054
29071
|
return result;
|
|
29055
29072
|
}
|
|
29056
29073
|
async listBlobs() {
|
|
29057
|
-
const res = await
|
|
29074
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs`, {
|
|
29058
29075
|
headers: this.getHeaders()
|
|
29059
29076
|
});
|
|
29060
29077
|
if (!res.ok)
|
|
@@ -29062,7 +29079,7 @@ class CcsiniClient {
|
|
|
29062
29079
|
return res.json();
|
|
29063
29080
|
}
|
|
29064
29081
|
async deleteBlobs(hashes) {
|
|
29065
|
-
const res = await
|
|
29082
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs/delete`, {
|
|
29066
29083
|
method: "POST",
|
|
29067
29084
|
headers: this.getHeaders(),
|
|
29068
29085
|
body: JSON.stringify({ hashes })
|
|
@@ -29081,7 +29098,7 @@ class CcsiniClient {
|
|
|
29081
29098
|
throw new Error("Failed to wipe account data");
|
|
29082
29099
|
}
|
|
29083
29100
|
async putProjects(projects, device) {
|
|
29084
|
-
const res = await
|
|
29101
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/projects`, {
|
|
29085
29102
|
method: "PUT",
|
|
29086
29103
|
headers: this.getHeaders(),
|
|
29087
29104
|
body: JSON.stringify({ projects, device, timestamp: Date.now() })
|
|
@@ -29091,6 +29108,17 @@ class CcsiniClient {
|
|
|
29091
29108
|
throw new Error(`Failed to send projects (${res.status}): ${text}`);
|
|
29092
29109
|
}
|
|
29093
29110
|
}
|
|
29111
|
+
async checkBlobsExist(hashes) {
|
|
29112
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/blobs/exists`, {
|
|
29113
|
+
method: "POST",
|
|
29114
|
+
headers: this.getHeaders(),
|
|
29115
|
+
body: JSON.stringify({ hashes })
|
|
29116
|
+
});
|
|
29117
|
+
if (!res.ok)
|
|
29118
|
+
throw new Error("Failed to check blob existence");
|
|
29119
|
+
const data = await res.json();
|
|
29120
|
+
return data.exists;
|
|
29121
|
+
}
|
|
29094
29122
|
async resetAll() {
|
|
29095
29123
|
const res = await fetch(`${this.apiUrl}/api/sync/reset`, {
|
|
29096
29124
|
method: "DELETE",
|
|
@@ -29101,7 +29129,7 @@ class CcsiniClient {
|
|
|
29101
29129
|
return res.json();
|
|
29102
29130
|
}
|
|
29103
29131
|
async heartbeat(mode, stats) {
|
|
29104
|
-
const res = await
|
|
29132
|
+
const res = await this.fetchWithRetry(`${this.apiUrl}/api/sync/heartbeat`, {
|
|
29105
29133
|
method: "POST",
|
|
29106
29134
|
headers: this.getHeaders(),
|
|
29107
29135
|
body: JSON.stringify({
|
|
@@ -29123,7 +29151,7 @@ class CcsiniClient {
|
|
|
29123
29151
|
return {};
|
|
29124
29152
|
}
|
|
29125
29153
|
async logSyncEvent(event) {
|
|
29126
|
-
await
|
|
29154
|
+
await this.fetchWithRetry(`${this.apiUrl}/api/sync/log`, {
|
|
29127
29155
|
method: "POST",
|
|
29128
29156
|
headers: this.getHeaders(),
|
|
29129
29157
|
body: JSON.stringify(event)
|
|
@@ -29136,6 +29164,7 @@ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir2 } from
|
|
|
29136
29164
|
import { join as join6, dirname } from "path";
|
|
29137
29165
|
import { homedir as homedir3 } from "os";
|
|
29138
29166
|
import { createHash as createHash2 } from "crypto";
|
|
29167
|
+
import { gzipSync, gunzipSync } from "zlib";
|
|
29139
29168
|
|
|
29140
29169
|
// src/core/manifest.ts
|
|
29141
29170
|
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -29402,6 +29431,30 @@ init_src();
|
|
|
29402
29431
|
function blobKey(filePath, contentHash) {
|
|
29403
29432
|
return createHash2("sha256").update(`${filePath}:${contentHash}`).digest("hex");
|
|
29404
29433
|
}
|
|
29434
|
+
var COMPRESS_FLAG = 1;
|
|
29435
|
+
var RAW_FLAG = 0;
|
|
29436
|
+
function compressForUpload(plaintext) {
|
|
29437
|
+
const compressed = gzipSync(plaintext);
|
|
29438
|
+
if (compressed.length < plaintext.length) {
|
|
29439
|
+
const result2 = new Uint8Array(1 + compressed.length);
|
|
29440
|
+
result2[0] = COMPRESS_FLAG;
|
|
29441
|
+
result2.set(compressed, 1);
|
|
29442
|
+
return result2;
|
|
29443
|
+
}
|
|
29444
|
+
const result = new Uint8Array(1 + plaintext.length);
|
|
29445
|
+
result[0] = RAW_FLAG;
|
|
29446
|
+
result.set(plaintext, 1);
|
|
29447
|
+
return result;
|
|
29448
|
+
}
|
|
29449
|
+
function decompressFromDownload(data) {
|
|
29450
|
+
if (data.length === 0)
|
|
29451
|
+
return data;
|
|
29452
|
+
if (data[0] === COMPRESS_FLAG)
|
|
29453
|
+
return gunzipSync(data.slice(1));
|
|
29454
|
+
if (data[0] === RAW_FLAG)
|
|
29455
|
+
return data.slice(1);
|
|
29456
|
+
return data;
|
|
29457
|
+
}
|
|
29405
29458
|
function getClaudeDir() {
|
|
29406
29459
|
return join6(homedir3(), ".claude");
|
|
29407
29460
|
}
|
|
@@ -29438,11 +29491,13 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29438
29491
|
const conflicts = [];
|
|
29439
29492
|
let bytesTransferred = 0;
|
|
29440
29493
|
const progress = onProgress ?? (() => {});
|
|
29441
|
-
|
|
29494
|
+
progress("Scanning local files & fetching remote manifest...");
|
|
29495
|
+
const [{ manifest: localManifest, cacheStats }, remoteManifestEnc] = await Promise.all([
|
|
29496
|
+
generateManifest(claudeDir, deviceName, progress, sessionOptions),
|
|
29497
|
+
client.getManifest()
|
|
29498
|
+
]);
|
|
29442
29499
|
const fileCount = Object.keys(localManifest.files).length;
|
|
29443
29500
|
progress(`Scanned ${fileCount} files`);
|
|
29444
|
-
progress("Fetching remote manifest...");
|
|
29445
|
-
const remoteManifestEnc = await client.getManifest();
|
|
29446
29501
|
let remoteManifest = null;
|
|
29447
29502
|
if (remoteManifestEnc) {
|
|
29448
29503
|
try {
|
|
@@ -29456,6 +29511,17 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29456
29511
|
const diffs = diffManifests(localManifest, remoteManifest);
|
|
29457
29512
|
const toPush = diffs.filter((d) => d.action === "push" || d.action === "merge");
|
|
29458
29513
|
progress(`${toPush.length} files to push`);
|
|
29514
|
+
if (toPush.length === 0) {
|
|
29515
|
+
return {
|
|
29516
|
+
action: "push",
|
|
29517
|
+
filesChanged: 0,
|
|
29518
|
+
bytesTransferred: 0,
|
|
29519
|
+
durationMs: Date.now() - start,
|
|
29520
|
+
errors: [],
|
|
29521
|
+
conflicts: [],
|
|
29522
|
+
cacheStats
|
|
29523
|
+
};
|
|
29524
|
+
}
|
|
29459
29525
|
if (remoteManifest && !pushOptions?.skipConflictBackup) {
|
|
29460
29526
|
const merges = toPush.filter((d) => d.action === "merge" && d.remoteHash);
|
|
29461
29527
|
for (const diff of merges) {
|
|
@@ -29466,7 +29532,8 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29466
29532
|
} catch {
|
|
29467
29533
|
encrypted = await client.downloadBlob(diff.remoteHash);
|
|
29468
29534
|
}
|
|
29469
|
-
const
|
|
29535
|
+
const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
|
|
29536
|
+
const decrypted = decompressFromDownload(decryptedRaw);
|
|
29470
29537
|
const conflictPath = join6(claudeDir, `${diff.path}.conflict`);
|
|
29471
29538
|
await mkdir2(dirname(conflictPath), { recursive: true });
|
|
29472
29539
|
await writeFile5(conflictPath, decrypted);
|
|
@@ -29475,30 +29542,32 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29475
29542
|
} catch {}
|
|
29476
29543
|
}
|
|
29477
29544
|
}
|
|
29545
|
+
const pushKeys = toPush.map((d) => blobKey(d.path, d.localHash));
|
|
29546
|
+
let existing = {};
|
|
29547
|
+
try {
|
|
29548
|
+
const batchResults = await Promise.all(chunkArray(pushKeys, 100).map((batch) => client.checkBlobsExist(batch)));
|
|
29549
|
+
for (const r of batchResults)
|
|
29550
|
+
Object.assign(existing, r);
|
|
29551
|
+
} catch {}
|
|
29552
|
+
const toUpload = toPush.filter((d) => !existing[blobKey(d.path, d.localHash)]);
|
|
29553
|
+
progress(`${toPush.length} changed, ${toUpload.length} to upload (${toPush.length - toUpload.length} skipped)`);
|
|
29478
29554
|
let uploaded = 0;
|
|
29479
29555
|
const failedPaths = new Set;
|
|
29480
|
-
const chunks = chunkArray(
|
|
29556
|
+
const chunks = chunkArray(toUpload, MAX_CONCURRENT_TRANSFERS);
|
|
29481
29557
|
for (const chunk of chunks) {
|
|
29482
29558
|
await Promise.all(chunk.map(async (diff) => {
|
|
29483
|
-
|
|
29484
|
-
|
|
29485
|
-
|
|
29486
|
-
|
|
29487
|
-
|
|
29488
|
-
|
|
29489
|
-
|
|
29490
|
-
|
|
29491
|
-
|
|
29492
|
-
|
|
29493
|
-
|
|
29494
|
-
}
|
|
29495
|
-
if (attempt < MAX_RETRIES) {
|
|
29496
|
-
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
29497
|
-
} else {
|
|
29498
|
-
failedPaths.add(diff.path);
|
|
29499
|
-
errors2.push(`Push ${diff.path}: ${e.message}`);
|
|
29500
|
-
}
|
|
29501
|
-
}
|
|
29559
|
+
try {
|
|
29560
|
+
const filePath = join6(claudeDir, diff.path);
|
|
29561
|
+
const content = await readFile6(filePath);
|
|
29562
|
+
const compressed = compressForUpload(content);
|
|
29563
|
+
const encrypted = encryptFile(masterKey, diff.path, compressed);
|
|
29564
|
+
await client.uploadBlob(blobKey(diff.path, diff.localHash), encrypted);
|
|
29565
|
+
bytesTransferred += encrypted.length;
|
|
29566
|
+
uploaded++;
|
|
29567
|
+
progress(`Uploading ${uploaded}/${toUpload.length}: ${diff.path}`);
|
|
29568
|
+
} catch (e) {
|
|
29569
|
+
failedPaths.add(diff.path);
|
|
29570
|
+
errors2.push(`Push ${diff.path}: ${e.message}`);
|
|
29502
29571
|
}
|
|
29503
29572
|
}));
|
|
29504
29573
|
}
|
|
@@ -29510,22 +29579,21 @@ async function pushSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29510
29579
|
}
|
|
29511
29580
|
const manifestJson = JSON.stringify(localManifest);
|
|
29512
29581
|
const manifestEnc = encryptFile(masterKey, "__manifest__", new TextEncoder().encode(manifestJson));
|
|
29513
|
-
await
|
|
29514
|
-
|
|
29582
|
+
await Promise.all([
|
|
29583
|
+
client.putManifest(manifestEnc),
|
|
29584
|
+
saveManifest(configDir, localManifest)
|
|
29585
|
+
]);
|
|
29515
29586
|
const durationMs = Date.now() - start;
|
|
29516
29587
|
progress("Logging sync event...");
|
|
29517
|
-
await
|
|
29518
|
-
|
|
29519
|
-
|
|
29520
|
-
|
|
29521
|
-
|
|
29522
|
-
|
|
29523
|
-
|
|
29524
|
-
|
|
29525
|
-
|
|
29526
|
-
await client.putProjects(projects, deviceName);
|
|
29527
|
-
}
|
|
29528
|
-
} catch {}
|
|
29588
|
+
await Promise.all([
|
|
29589
|
+
client.logSyncEvent({
|
|
29590
|
+
action: "push",
|
|
29591
|
+
filesChanged: toPush.length,
|
|
29592
|
+
bytesTransferred,
|
|
29593
|
+
durationMs
|
|
29594
|
+
}).catch(() => {}),
|
|
29595
|
+
extractAndSendProjects(client, localManifest, deviceName).catch(() => {})
|
|
29596
|
+
]);
|
|
29529
29597
|
return {
|
|
29530
29598
|
action: "push",
|
|
29531
29599
|
filesChanged: toPush.length,
|
|
@@ -29579,56 +29647,84 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29579
29647
|
const toPull = diffs.filter((d) => (d.action === "pull" || d.action === "merge") && !isExcluded(d.path) && (sessionsEnabled || !isSessionFile(d.path)));
|
|
29580
29648
|
progress(`${toPull.length} files to pull`);
|
|
29581
29649
|
let downloaded = 0;
|
|
29582
|
-
const
|
|
29583
|
-
for (const
|
|
29584
|
-
|
|
29585
|
-
|
|
29586
|
-
|
|
29650
|
+
const keyToDiffs = new Map;
|
|
29651
|
+
for (const diff of toPull) {
|
|
29652
|
+
const key = blobKey(diff.path, diff.remoteHash);
|
|
29653
|
+
const list = keyToDiffs.get(key) ?? [];
|
|
29654
|
+
list.push(diff);
|
|
29655
|
+
keyToDiffs.set(key, list);
|
|
29656
|
+
}
|
|
29657
|
+
const uniqueKeys = Array.from(keyToDiffs.keys());
|
|
29658
|
+
const batches = chunkArray(uniqueKeys, BATCH_DOWNLOAD_SIZE);
|
|
29659
|
+
const downloadedBlobs = new Map;
|
|
29660
|
+
for (const batch of batches) {
|
|
29661
|
+
try {
|
|
29662
|
+
const results = await client.batchDownload(batch);
|
|
29663
|
+
for (const [key, data] of Object.entries(results)) {
|
|
29664
|
+
if (data)
|
|
29665
|
+
downloadedBlobs.set(key, data);
|
|
29666
|
+
}
|
|
29667
|
+
} catch {
|
|
29668
|
+
for (const key of batch) {
|
|
29669
|
+
try {
|
|
29670
|
+
const data = await client.downloadBlob(key);
|
|
29671
|
+
downloadedBlobs.set(key, data);
|
|
29672
|
+
} catch {}
|
|
29673
|
+
}
|
|
29674
|
+
}
|
|
29675
|
+
}
|
|
29676
|
+
for (const diff of toPull) {
|
|
29677
|
+
try {
|
|
29678
|
+
const key = blobKey(diff.path, diff.remoteHash);
|
|
29679
|
+
let encrypted = downloadedBlobs.get(key);
|
|
29680
|
+
if (!encrypted) {
|
|
29587
29681
|
try {
|
|
29588
|
-
encrypted = await client.downloadBlob(blobKey(diff.path, diff.remoteHash));
|
|
29589
|
-
} catch {
|
|
29590
29682
|
encrypted = await client.downloadBlob(diff.remoteHash);
|
|
29683
|
+
} catch {
|
|
29684
|
+
errors2.push(`Pull ${diff.path}: blob not found`);
|
|
29685
|
+
continue;
|
|
29591
29686
|
}
|
|
29592
|
-
|
|
29593
|
-
|
|
29594
|
-
|
|
29595
|
-
|
|
29596
|
-
|
|
29597
|
-
|
|
29598
|
-
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
}
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
|
|
29608
|
-
|
|
29609
|
-
|
|
29610
|
-
|
|
29611
|
-
|
|
29612
|
-
|
|
29613
|
-
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29614
|
-
return;
|
|
29615
|
-
}
|
|
29616
|
-
await writeFile5(`${localPath2}.conflict`, localContent);
|
|
29617
|
-
await writeFile5(localPath2, remoteContent);
|
|
29687
|
+
}
|
|
29688
|
+
bytesTransferred += encrypted.length;
|
|
29689
|
+
const decryptedRaw = decryptFile(masterKey, diff.path, encrypted);
|
|
29690
|
+
const decrypted = decompressFromDownload(decryptedRaw);
|
|
29691
|
+
if (diff.action === "merge" && diff.localHash) {
|
|
29692
|
+
const localPath2 = join6(claudeDir, diff.path);
|
|
29693
|
+
const localContent = await readFile6(localPath2, "utf-8");
|
|
29694
|
+
const remoteContent = new TextDecoder().decode(decrypted);
|
|
29695
|
+
if (diff.category === "session") {
|
|
29696
|
+
const merged = mergeSessionFiles(localContent, remoteContent);
|
|
29697
|
+
await writeFile5(localPath2, merged);
|
|
29698
|
+
downloaded++;
|
|
29699
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29700
|
+
continue;
|
|
29701
|
+
}
|
|
29702
|
+
const entry = remoteManifest.files[diff.path];
|
|
29703
|
+
const localEntry = localManifest?.files[diff.path];
|
|
29704
|
+
if (localEntry && entry) {
|
|
29705
|
+
const result = mergeLastWriteWins(localContent, remoteContent, localEntry.modified, entry.modified);
|
|
29706
|
+
await writeFile5(localPath2, result.content);
|
|
29707
|
+
await writeFile5(`${localPath2}.conflict`, result.backupContent);
|
|
29618
29708
|
conflicts.push(diff.path);
|
|
29619
29709
|
downloaded++;
|
|
29620
|
-
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}
|
|
29621
|
-
|
|
29710
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29711
|
+
continue;
|
|
29622
29712
|
}
|
|
29623
|
-
|
|
29624
|
-
await
|
|
29625
|
-
|
|
29713
|
+
await writeFile5(`${localPath2}.conflict`, localContent);
|
|
29714
|
+
await writeFile5(localPath2, remoteContent);
|
|
29715
|
+
conflicts.push(diff.path);
|
|
29626
29716
|
downloaded++;
|
|
29627
|
-
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29628
|
-
|
|
29629
|
-
errors2.push(`Pull ${diff.path}: ${e.message}`);
|
|
29717
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path} (conflict saved)`);
|
|
29718
|
+
continue;
|
|
29630
29719
|
}
|
|
29631
|
-
|
|
29720
|
+
const localPath = join6(claudeDir, diff.path);
|
|
29721
|
+
await mkdir2(dirname(localPath), { recursive: true });
|
|
29722
|
+
await writeFile5(localPath, decrypted);
|
|
29723
|
+
downloaded++;
|
|
29724
|
+
progress(`Downloading ${downloaded}/${toPull.length}: ${diff.path}`);
|
|
29725
|
+
} catch (e) {
|
|
29726
|
+
errors2.push(`Pull ${diff.path}: ${e.message}`);
|
|
29727
|
+
}
|
|
29632
29728
|
}
|
|
29633
29729
|
progress("Saving manifest...");
|
|
29634
29730
|
await saveManifest(configDir, remoteManifest);
|
|
@@ -29639,7 +29735,7 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29639
29735
|
filesChanged: toPull.length,
|
|
29640
29736
|
bytesTransferred,
|
|
29641
29737
|
durationMs
|
|
29642
|
-
});
|
|
29738
|
+
}).catch(() => {});
|
|
29643
29739
|
return {
|
|
29644
29740
|
action: "pull",
|
|
29645
29741
|
filesChanged: toPull.length,
|
|
@@ -29649,6 +29745,12 @@ async function pullSync(client, masterKey, deviceName, configDir, onProgress, se
|
|
|
29649
29745
|
conflicts
|
|
29650
29746
|
};
|
|
29651
29747
|
}
|
|
29748
|
+
async function extractAndSendProjects(client, manifest, deviceName) {
|
|
29749
|
+
const projects = extractProjects(manifest);
|
|
29750
|
+
if (projects.length > 0) {
|
|
29751
|
+
await client.putProjects(projects, deviceName);
|
|
29752
|
+
}
|
|
29753
|
+
}
|
|
29652
29754
|
function chunkArray(arr, size) {
|
|
29653
29755
|
const chunks = [];
|
|
29654
29756
|
for (let i = 0;i < arr.length; i += size) {
|