hsync-cli 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/dist/bin.js +596 -245
- package/dist/web/index.html +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
File without changes
|
package/dist/bin.js
CHANGED
|
@@ -69670,16 +69670,15 @@ async function scanLocalPath(targetPath, rootDir) {
|
|
|
69670
69670
|
function parseRemoteTarget(target) {
|
|
69671
69671
|
const parts = target.split(":");
|
|
69672
69672
|
const slug = parts[0];
|
|
69673
|
-
const
|
|
69674
|
-
|
|
69673
|
+
const rawPath = parts.slice(1).join(":") || "";
|
|
69674
|
+
const endsWithSlash = rawPath.endsWith("/");
|
|
69675
|
+
const remotePath = rawPath.replace(/^\/+|\/+$/g, "");
|
|
69676
|
+
return { slug, remotePath, endsWithSlash };
|
|
69675
69677
|
}
|
|
69676
69678
|
async function push(localPath, remoteTarget, options) {
|
|
69677
|
-
const forceYes = options.yes || false;
|
|
69678
69679
|
try {
|
|
69679
69680
|
const config = await loadConfig();
|
|
69680
|
-
|
|
69681
|
-
const apiKey = config.apiKey;
|
|
69682
|
-
if (!serverUrl || !apiKey) {
|
|
69681
|
+
if (!config.serverUrl || !config.apiKey) {
|
|
69683
69682
|
console.error(source_default.red("Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login"));
|
|
69684
69683
|
process.exit(1);
|
|
69685
69684
|
}
|
|
@@ -69694,22 +69693,28 @@ async function push(localPath, remoteTarget, options) {
|
|
|
69694
69693
|
const spinner = ora2("Scanning local files...").start();
|
|
69695
69694
|
const localFiles = await scanLocalPath(resolvedLocalPath, resolvedLocalPath);
|
|
69696
69695
|
spinner.text = "Fetching remote file tree...";
|
|
69697
|
-
const remoteClient = new RemoteClient(serverUrl, apiKey);
|
|
69696
|
+
const remoteClient = new RemoteClient(config.serverUrl, config.apiKey);
|
|
69698
69697
|
const remoteFiles = await remoteClient.getTree(remote.slug, remote.remotePath);
|
|
69699
69698
|
spinner.stop();
|
|
69700
69699
|
console.log(source_default.cyan(`Found ${localFiles.length} local files, ${remoteFiles.length} remote files.`));
|
|
69701
|
-
|
|
69700
|
+
let isRemoteDir = !(await fs3.stat(resolvedLocalPath)).isFile() || remote.endsWithSlash;
|
|
69701
|
+
if (!isRemoteDir) {
|
|
69702
|
+
if (remoteFiles.length > 1) {
|
|
69703
|
+
isRemoteDir = true;
|
|
69704
|
+
} else if (remoteFiles.length === 1 && remoteFiles[0].path !== "") {
|
|
69705
|
+
isRemoteDir = true;
|
|
69706
|
+
}
|
|
69707
|
+
}
|
|
69708
|
+
const prompter = new OverwritePrompter(options.yes || false);
|
|
69702
69709
|
let pushCount = 0;
|
|
69703
69710
|
let skipCount = 0;
|
|
69704
69711
|
for (const localFile of localFiles) {
|
|
69705
|
-
const remoteFile = remoteFiles.find((f) => f.path === localFile.relativePath);
|
|
69706
|
-
const destinationPath = remote.remotePath ? `${remote.remotePath}/${localFile.relativePath}` : localFile.relativePath;
|
|
69712
|
+
const remoteFile = remoteFiles.find((f) => f.path === (isRemoteDir ? localFile.relativePath : ""));
|
|
69707
69713
|
let shouldUpload = false;
|
|
69708
69714
|
if (!remoteFile) {
|
|
69709
69715
|
shouldUpload = true;
|
|
69710
69716
|
} else if (remoteFile.hash !== localFile.hash) {
|
|
69711
|
-
|
|
69712
|
-
if (confirm) {
|
|
69717
|
+
if (await prompter.shouldOverwrite(localFile.relativePath)) {
|
|
69713
69718
|
shouldUpload = true;
|
|
69714
69719
|
} else {
|
|
69715
69720
|
skipCount++;
|
|
@@ -69720,7 +69725,7 @@ async function push(localPath, remoteTarget, options) {
|
|
|
69720
69725
|
if (shouldUpload) {
|
|
69721
69726
|
const fileSpinner = ora2(`Pushing ${localFile.relativePath}...`).start();
|
|
69722
69727
|
try {
|
|
69723
|
-
await remoteClient.uploadFile(remote.slug,
|
|
69728
|
+
await remoteClient.uploadFile(remote.slug, isRemoteDir ? remote.remotePath ? `${remote.remotePath}/${localFile.relativePath}` : localFile.relativePath : remote.remotePath, localFile.absolutePath);
|
|
69724
69729
|
fileSpinner.succeed(source_default.green(`Pushed ${localFile.relativePath}`));
|
|
69725
69730
|
pushCount++;
|
|
69726
69731
|
} catch (uploadErr) {
|
|
@@ -69736,38 +69741,65 @@ async function push(localPath, remoteTarget, options) {
|
|
|
69736
69741
|
}
|
|
69737
69742
|
}
|
|
69738
69743
|
async function pull(remoteTarget, localPath, options) {
|
|
69739
|
-
const forceYes = options.yes || false;
|
|
69740
69744
|
try {
|
|
69741
69745
|
const config = await loadConfig();
|
|
69742
|
-
|
|
69743
|
-
const apiKey = config.apiKey;
|
|
69744
|
-
if (!serverUrl || !apiKey) {
|
|
69746
|
+
if (!config.serverUrl || !config.apiKey) {
|
|
69745
69747
|
console.error(source_default.red("Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login"));
|
|
69746
69748
|
process.exit(1);
|
|
69747
69749
|
}
|
|
69748
69750
|
const remote = parseRemoteTarget(remoteTarget);
|
|
69749
69751
|
const resolvedLocalPath = path4.resolve(localPath);
|
|
69750
69752
|
const spinner = ora2("Fetching remote file tree...").start();
|
|
69751
|
-
const remoteClient = new RemoteClient(serverUrl, apiKey);
|
|
69753
|
+
const remoteClient = new RemoteClient(config.serverUrl, config.apiKey);
|
|
69752
69754
|
const remoteFiles = await remoteClient.getTree(remote.slug, remote.remotePath);
|
|
69753
69755
|
spinner.text = "Scanning local files...";
|
|
69754
|
-
|
|
69755
|
-
|
|
69756
|
+
const isRemoteFile = remoteFiles.length === 1 && remoteFiles[0].path === "";
|
|
69757
|
+
let isLocalDir = false;
|
|
69758
|
+
try {
|
|
69759
|
+
const localStat = await fs3.stat(resolvedLocalPath);
|
|
69760
|
+
isLocalDir = localStat.isDirectory();
|
|
69761
|
+
} catch {
|
|
69762
|
+
isLocalDir = localPath.endsWith("/") || localPath.endsWith(path4.sep);
|
|
69763
|
+
}
|
|
69764
|
+
if (isRemoteFile && !isLocalDir) {
|
|
69765
|
+
await fs3.mkdir(path4.dirname(resolvedLocalPath), { recursive: true });
|
|
69766
|
+
} else {
|
|
69767
|
+
await fs3.mkdir(resolvedLocalPath, { recursive: true });
|
|
69768
|
+
}
|
|
69769
|
+
let localFiles = [];
|
|
69770
|
+
try {
|
|
69771
|
+
localFiles = await scanLocalPath(resolvedLocalPath, resolvedLocalPath);
|
|
69772
|
+
} catch (err) {
|
|
69773
|
+
if (err.code !== "ENOENT") {
|
|
69774
|
+
throw err;
|
|
69775
|
+
}
|
|
69776
|
+
}
|
|
69756
69777
|
spinner.stop();
|
|
69757
69778
|
console.log(source_default.cyan(`Found ${remoteFiles.length} remote files, ${localFiles.length} local files.`));
|
|
69758
|
-
const prompter = new OverwritePrompter(
|
|
69779
|
+
const prompter = new OverwritePrompter(options.yes || false);
|
|
69759
69780
|
let pullCount = 0;
|
|
69760
69781
|
let skipCount = 0;
|
|
69761
69782
|
for (const remoteFile of remoteFiles) {
|
|
69762
|
-
|
|
69763
|
-
|
|
69764
|
-
|
|
69783
|
+
let localDestPath;
|
|
69784
|
+
let localFile;
|
|
69785
|
+
if (isRemoteFile) {
|
|
69786
|
+
if (isLocalDir) {
|
|
69787
|
+
localDestPath = path4.join(resolvedLocalPath, path4.basename(remote.remotePath));
|
|
69788
|
+
localFile = localFiles.find((f) => f.relativePath === path4.basename(remote.remotePath));
|
|
69789
|
+
} else {
|
|
69790
|
+
localDestPath = resolvedLocalPath;
|
|
69791
|
+
localFile = localFiles.length > 0 ? localFiles[0] : undefined;
|
|
69792
|
+
}
|
|
69793
|
+
} else {
|
|
69794
|
+
localDestPath = path4.join(resolvedLocalPath, remoteFile.path);
|
|
69795
|
+
localFile = localFiles.find((f) => f.relativePath === remoteFile.path);
|
|
69796
|
+
}
|
|
69765
69797
|
let shouldDownload = false;
|
|
69766
69798
|
if (!localFile) {
|
|
69767
69799
|
shouldDownload = true;
|
|
69768
69800
|
} else if (localFile.hash !== remoteFile.hash) {
|
|
69769
|
-
const
|
|
69770
|
-
if (
|
|
69801
|
+
const promptPath = isRemoteFile ? remote.remotePath || path4.basename(localDestPath) : remoteFile.path;
|
|
69802
|
+
if (await prompter.shouldOverwrite(promptPath)) {
|
|
69771
69803
|
shouldDownload = true;
|
|
69772
69804
|
} else {
|
|
69773
69805
|
skipCount++;
|
|
@@ -69776,13 +69808,14 @@ async function pull(remoteTarget, localPath, options) {
|
|
|
69776
69808
|
skipCount++;
|
|
69777
69809
|
}
|
|
69778
69810
|
if (shouldDownload) {
|
|
69779
|
-
const
|
|
69811
|
+
const logPath = isRemoteFile ? remote.remotePath || path4.basename(localDestPath) : remoteFile.path;
|
|
69812
|
+
const fileSpinner = ora2(`Pulling ${logPath}...`).start();
|
|
69780
69813
|
try {
|
|
69781
|
-
await remoteClient.downloadFile(remote.slug,
|
|
69782
|
-
fileSpinner.succeed(source_default.green(`Pulled ${
|
|
69814
|
+
await remoteClient.downloadFile(remote.slug, isRemoteFile ? remote.remotePath : remote.remotePath ? `${remote.remotePath}/${remoteFile.path}` : remoteFile.path, localDestPath);
|
|
69815
|
+
fileSpinner.succeed(source_default.green(`Pulled ${logPath}`));
|
|
69783
69816
|
pullCount++;
|
|
69784
69817
|
} catch (downloadErr) {
|
|
69785
|
-
fileSpinner.fail(source_default.red(`Failed to pull ${
|
|
69818
|
+
fileSpinner.fail(source_default.red(`Failed to pull ${logPath}: ${downloadErr.message}`));
|
|
69786
69819
|
}
|
|
69787
69820
|
}
|
|
69788
69821
|
}
|
|
@@ -70153,6 +70186,177 @@ class SyncEngine {
|
|
|
70153
70186
|
}
|
|
70154
70187
|
}
|
|
70155
70188
|
// src/commands/sync.ts
|
|
70189
|
+
function getHsyncDir() {
|
|
70190
|
+
return path7.join(os4.homedir(), ".hsync");
|
|
70191
|
+
}
|
|
70192
|
+
function isProcessAlive(pid) {
|
|
70193
|
+
try {
|
|
70194
|
+
process.kill(pid, 0);
|
|
70195
|
+
return true;
|
|
70196
|
+
} catch {
|
|
70197
|
+
return false;
|
|
70198
|
+
}
|
|
70199
|
+
}
|
|
70200
|
+
function jobNameFromPidFile(filename) {
|
|
70201
|
+
if (filename === "daemon.pid") {
|
|
70202
|
+
return "all jobs";
|
|
70203
|
+
}
|
|
70204
|
+
return filename.replace("daemon-", "").replace(".pid", "");
|
|
70205
|
+
}
|
|
70206
|
+
async function readPidRecord(pidFile) {
|
|
70207
|
+
try {
|
|
70208
|
+
const pidText = await fs6.readFile(pidFile, "utf8");
|
|
70209
|
+
const pid = parseInt(pidText.trim(), 10);
|
|
70210
|
+
if (isNaN(pid)) {
|
|
70211
|
+
await fs6.unlink(pidFile);
|
|
70212
|
+
return null;
|
|
70213
|
+
}
|
|
70214
|
+
if (!isProcessAlive(pid)) {
|
|
70215
|
+
await fs6.unlink(pidFile);
|
|
70216
|
+
return null;
|
|
70217
|
+
}
|
|
70218
|
+
return { pid };
|
|
70219
|
+
} catch {
|
|
70220
|
+
return null;
|
|
70221
|
+
}
|
|
70222
|
+
}
|
|
70223
|
+
async function listAllDaemonPids() {
|
|
70224
|
+
const hsyncDir = getHsyncDir();
|
|
70225
|
+
const results = [];
|
|
70226
|
+
try {
|
|
70227
|
+
const files = await fs6.readdir(hsyncDir);
|
|
70228
|
+
for (const file of files) {
|
|
70229
|
+
if (!file.startsWith("daemon") || !file.endsWith(".pid")) {
|
|
70230
|
+
continue;
|
|
70231
|
+
}
|
|
70232
|
+
const pidFile = path7.join(hsyncDir, file);
|
|
70233
|
+
const record = await readPidRecord(pidFile);
|
|
70234
|
+
if (record) {
|
|
70235
|
+
results.push({
|
|
70236
|
+
pidFile,
|
|
70237
|
+
pid: record.pid,
|
|
70238
|
+
jobName: jobNameFromPidFile(file)
|
|
70239
|
+
});
|
|
70240
|
+
}
|
|
70241
|
+
}
|
|
70242
|
+
} catch {}
|
|
70243
|
+
return results;
|
|
70244
|
+
}
|
|
70245
|
+
var RECONCILE_INTERVAL_MS = 1e4;
|
|
70246
|
+
async function isGlobalDaemonRunning() {
|
|
70247
|
+
const globalFile = path7.join(getHsyncDir(), "daemon.pid");
|
|
70248
|
+
const record = await readPidRecord(globalFile);
|
|
70249
|
+
return record !== null;
|
|
70250
|
+
}
|
|
70251
|
+
async function executeSyncPass(jobId, activeJobs) {
|
|
70252
|
+
if (activeJobs.has(jobId)) {
|
|
70253
|
+
const config = await loadConfig();
|
|
70254
|
+
const busyJob = config.syncJobs?.find((j) => j.id === jobId);
|
|
70255
|
+
const busyName = busyJob?.name || jobId;
|
|
70256
|
+
console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${busyName}] Skip run — previous run still in progress.`));
|
|
70257
|
+
return;
|
|
70258
|
+
}
|
|
70259
|
+
const freshConfig = await loadConfig();
|
|
70260
|
+
const freshServerUrl = freshConfig.serverUrl;
|
|
70261
|
+
const freshApiKey = freshConfig.apiKey;
|
|
70262
|
+
const freshJob = freshConfig.syncJobs?.find((j) => j.id === jobId);
|
|
70263
|
+
if (!freshJob) {
|
|
70264
|
+
return;
|
|
70265
|
+
}
|
|
70266
|
+
if (!freshServerUrl || !freshApiKey) {
|
|
70267
|
+
console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Missing server URL or API key. Run: hsync login`));
|
|
70268
|
+
return;
|
|
70269
|
+
}
|
|
70270
|
+
if (!freshJob.enabled) {
|
|
70271
|
+
return;
|
|
70272
|
+
}
|
|
70273
|
+
activeJobs.add(jobId);
|
|
70274
|
+
const stateDbPath = path7.join(getHsyncDir(), "state", `${freshJob.id}.db`);
|
|
70275
|
+
const engine = new SyncEngine(freshJob, freshServerUrl, freshApiKey, stateDbPath);
|
|
70276
|
+
let uploadCount = 0;
|
|
70277
|
+
let downloadCount = 0;
|
|
70278
|
+
let skipCount = 0;
|
|
70279
|
+
let errorCount = 0;
|
|
70280
|
+
engine.on((event) => {
|
|
70281
|
+
const time = new Date().toLocaleTimeString();
|
|
70282
|
+
if (event.type === "start") {
|
|
70283
|
+
console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Running scan...`));
|
|
70284
|
+
} else if (event.type === "scan-complete") {
|
|
70285
|
+
console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Scan completed.`));
|
|
70286
|
+
} else if (event.type === "upload-complete") {
|
|
70287
|
+
uploadCount++;
|
|
70288
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Uploaded: ${event.path}`));
|
|
70289
|
+
} else if (event.type === "download-complete") {
|
|
70290
|
+
downloadCount++;
|
|
70291
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Downloaded: ${event.path}`));
|
|
70292
|
+
} else if (event.type === "skip") {
|
|
70293
|
+
skipCount++;
|
|
70294
|
+
} else if (event.type === "conflict") {
|
|
70295
|
+
console.log(source_default.yellow(`[${time}] [Sync: ${freshJob.name}] ⚠ Conflict resolved for: ${event.path}`));
|
|
70296
|
+
} else if (event.type === "error") {
|
|
70297
|
+
errorCount++;
|
|
70298
|
+
console.error(source_default.red(`[${time}] [Sync: ${freshJob.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
|
|
70299
|
+
} else if (event.type === "complete") {
|
|
70300
|
+
const summary = `${uploadCount} uploaded, ${downloadCount} downloaded, ${skipCount} unchanged`;
|
|
70301
|
+
if (uploadCount === 0 && downloadCount === 0 && errorCount === 0) {
|
|
70302
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed (${summary}).`));
|
|
70303
|
+
} else {
|
|
70304
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed successfully (${summary}).`));
|
|
70305
|
+
}
|
|
70306
|
+
}
|
|
70307
|
+
});
|
|
70308
|
+
try {
|
|
70309
|
+
await engine.run();
|
|
70310
|
+
} catch (runErr) {
|
|
70311
|
+
console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Sync crash: ${runErr.message}`));
|
|
70312
|
+
} finally {
|
|
70313
|
+
activeJobs.delete(jobId);
|
|
70314
|
+
}
|
|
70315
|
+
}
|
|
70316
|
+
function scheduleSyncJob(job, activeJobs, scheduledJobs) {
|
|
70317
|
+
const intervalDelay = job.intervalMs || 1e4;
|
|
70318
|
+
const triggerSyncRun = async () => {
|
|
70319
|
+
await executeSyncPass(job.id, activeJobs);
|
|
70320
|
+
};
|
|
70321
|
+
triggerSyncRun();
|
|
70322
|
+
const intervalId = setInterval(triggerSyncRun, intervalDelay);
|
|
70323
|
+
scheduledJobs.set(job.id, { intervalId, intervalMs: intervalDelay });
|
|
70324
|
+
console.log(source_default.green(`[${new Date().toLocaleTimeString()}] [Scheduler] Registered: ${job.name} (every ${intervalDelay / 1000}s)`));
|
|
70325
|
+
}
|
|
70326
|
+
function unscheduleSyncJob(jobId, jobName, scheduledJobs) {
|
|
70327
|
+
const entry = scheduledJobs.get(jobId);
|
|
70328
|
+
if (!entry) {
|
|
70329
|
+
return;
|
|
70330
|
+
}
|
|
70331
|
+
clearInterval(entry.intervalId);
|
|
70332
|
+
scheduledJobs.delete(jobId);
|
|
70333
|
+
console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Scheduler] Unregistered: ${jobName || jobId}`));
|
|
70334
|
+
}
|
|
70335
|
+
async function reconcileSchedules(activeJobs, scheduledJobs, singleJobName) {
|
|
70336
|
+
const config = await loadConfig();
|
|
70337
|
+
let jobs = (config.syncJobs || []).filter((j) => j.enabled);
|
|
70338
|
+
if (singleJobName) {
|
|
70339
|
+
jobs = jobs.filter((j) => j.name === singleJobName);
|
|
70340
|
+
}
|
|
70341
|
+
const enabledIds = new Set(jobs.map((j) => j.id));
|
|
70342
|
+
for (const [jobId, entry] of scheduledJobs) {
|
|
70343
|
+
if (!enabledIds.has(jobId)) {
|
|
70344
|
+
const staleJob = config.syncJobs?.find((j) => j.id === jobId);
|
|
70345
|
+
unscheduleSyncJob(jobId, staleJob?.name, scheduledJobs);
|
|
70346
|
+
continue;
|
|
70347
|
+
}
|
|
70348
|
+
const job = jobs.find((j) => j.id === jobId);
|
|
70349
|
+
if (job && entry.intervalMs !== (job.intervalMs || 1e4)) {
|
|
70350
|
+
unscheduleSyncJob(jobId, job.name, scheduledJobs);
|
|
70351
|
+
scheduleSyncJob(job, activeJobs, scheduledJobs);
|
|
70352
|
+
}
|
|
70353
|
+
}
|
|
70354
|
+
for (const job of jobs) {
|
|
70355
|
+
if (!scheduledJobs.has(job.id)) {
|
|
70356
|
+
scheduleSyncJob(job, activeJobs, scheduledJobs);
|
|
70357
|
+
}
|
|
70358
|
+
}
|
|
70359
|
+
}
|
|
70156
70360
|
function parseRemoteTarget2(target) {
|
|
70157
70361
|
const parts = target.split(":");
|
|
70158
70362
|
const slug = parts[0];
|
|
@@ -70201,6 +70405,11 @@ async function addSyncJob(localPath, remoteTarget, options) {
|
|
|
70201
70405
|
console.log(`Direction: ${newJob.direction}`);
|
|
70202
70406
|
console.log(`Strategy: ${newJob.conflictStrategy}`);
|
|
70203
70407
|
console.log(`Interval: ${intervalSec} seconds`);
|
|
70408
|
+
if (await isGlobalDaemonRunning()) {
|
|
70409
|
+
console.log(source_default.gray("The running sync daemon will pick up this job automatically."));
|
|
70410
|
+
} else {
|
|
70411
|
+
console.log(source_default.gray("Start all sync jobs with: hsync sync start --daemon"));
|
|
70412
|
+
}
|
|
70204
70413
|
} catch (err) {
|
|
70205
70414
|
console.error(source_default.red(`Error: Failed to register sync job: ${err.message}`));
|
|
70206
70415
|
process.exit(1);
|
|
@@ -70249,37 +70458,44 @@ async function removeSyncJob(name) {
|
|
|
70249
70458
|
}
|
|
70250
70459
|
async function startSyncScheduler(name, options) {
|
|
70251
70460
|
const isDaemon = options?.daemon;
|
|
70252
|
-
|
|
70253
|
-
const
|
|
70461
|
+
let foregroundJobName = name;
|
|
70462
|
+
const hsyncDir = getHsyncDir();
|
|
70463
|
+
const globalPidFile = path7.join(hsyncDir, "daemon.pid");
|
|
70464
|
+
if (isDaemon && foregroundJobName) {
|
|
70465
|
+
console.log(source_default.yellow(`The sync daemon manages all enabled jobs. Starting global daemon instead of "${foregroundJobName}" only.`));
|
|
70466
|
+
foregroundJobName = undefined;
|
|
70467
|
+
}
|
|
70254
70468
|
if (isDaemon) {
|
|
70255
70469
|
await fs6.mkdir(hsyncDir, { recursive: true });
|
|
70256
|
-
|
|
70257
|
-
|
|
70258
|
-
|
|
70259
|
-
|
|
70260
|
-
|
|
70261
|
-
|
|
70262
|
-
|
|
70263
|
-
|
|
70264
|
-
|
|
70265
|
-
|
|
70266
|
-
}
|
|
70267
|
-
const logFile = path7.join(hsyncDir,
|
|
70268
|
-
console.log(source_default.cyan(`⚙️ Starting sync daemon in the background...`));
|
|
70269
|
-
const
|
|
70270
|
-
const
|
|
70271
|
-
const
|
|
70470
|
+
const existing = await readPidRecord(globalPidFile);
|
|
70471
|
+
if (existing) {
|
|
70472
|
+
console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${existing.pid}).`));
|
|
70473
|
+
console.log(source_default.gray("New sync jobs are picked up automatically — no restart needed."));
|
|
70474
|
+
return;
|
|
70475
|
+
}
|
|
70476
|
+
const legacyDaemons = await listAllDaemonPids();
|
|
70477
|
+
const perJobDaemons = legacyDaemons.filter((d) => d.jobName !== "all jobs");
|
|
70478
|
+
if (perJobDaemons.length > 0) {
|
|
70479
|
+
console.log(source_default.yellow("Warning: Legacy per-job daemons are still running. Stop them with: hsync sync stop"));
|
|
70480
|
+
}
|
|
70481
|
+
const logFile = path7.join(hsyncDir, "daemon.log");
|
|
70482
|
+
console.log(source_default.cyan(`⚙️ Starting global sync daemon in the background...`));
|
|
70483
|
+
const bunExec = process.execPath;
|
|
70484
|
+
const scriptPath = Bun.main;
|
|
70485
|
+
const spawnArgs = ["sync", "start"];
|
|
70486
|
+
const logFileHandle = await fs6.open(logFile, "a");
|
|
70487
|
+
const subprocess = Bun.spawn([bunExec, scriptPath, ...spawnArgs], {
|
|
70272
70488
|
detached: true,
|
|
70273
70489
|
stdout: logFileHandle.fd,
|
|
70274
70490
|
stderr: logFileHandle.fd,
|
|
70275
70491
|
stdin: "ignore"
|
|
70276
70492
|
});
|
|
70277
|
-
const pid = subprocess.pid;
|
|
70278
|
-
await fs6.writeFile(pidFile, pid.toString(), "utf8");
|
|
70279
70493
|
subprocess.unref();
|
|
70280
|
-
|
|
70281
|
-
console.log(
|
|
70282
|
-
console.log(
|
|
70494
|
+
await logFileHandle.close();
|
|
70495
|
+
console.log(source_default.green(`✔ Global sync daemon started successfully!`));
|
|
70496
|
+
console.log(source_default.gray("All enabled sync jobs will be scheduled automatically."));
|
|
70497
|
+
console.log(`Logs: hsync sync logs -f`);
|
|
70498
|
+
console.log(source_default.gray("Stop with: hsync sync stop"));
|
|
70283
70499
|
return;
|
|
70284
70500
|
}
|
|
70285
70501
|
try {
|
|
@@ -70290,11 +70506,12 @@ async function startSyncScheduler(name, options) {
|
|
|
70290
70506
|
console.error(source_default.red("Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login"));
|
|
70291
70507
|
process.exit(1);
|
|
70292
70508
|
}
|
|
70509
|
+
const isGlobalScheduler = !foregroundJobName;
|
|
70293
70510
|
let jobs = config.syncJobs || [];
|
|
70294
|
-
if (
|
|
70295
|
-
jobs = jobs.filter((j) => j.name ===
|
|
70511
|
+
if (foregroundJobName) {
|
|
70512
|
+
jobs = jobs.filter((j) => j.name === foregroundJobName);
|
|
70296
70513
|
if (jobs.length === 0) {
|
|
70297
|
-
console.error(source_default.red(`Error: Sync job named "${
|
|
70514
|
+
console.error(source_default.red(`Error: Sync job named "${foregroundJobName}" was not found or is disabled.`));
|
|
70298
70515
|
process.exit(1);
|
|
70299
70516
|
}
|
|
70300
70517
|
} else {
|
|
@@ -70305,10 +70522,10 @@ async function startSyncScheduler(name, options) {
|
|
|
70305
70522
|
return;
|
|
70306
70523
|
}
|
|
70307
70524
|
await fs6.mkdir(hsyncDir, { recursive: true });
|
|
70308
|
-
await fs6.writeFile(
|
|
70525
|
+
await fs6.writeFile(globalPidFile, process.pid.toString(), "utf8");
|
|
70309
70526
|
const cleanPidFile = async () => {
|
|
70310
70527
|
try {
|
|
70311
|
-
await fs6.unlink(
|
|
70528
|
+
await fs6.unlink(globalPidFile);
|
|
70312
70529
|
} catch {}
|
|
70313
70530
|
};
|
|
70314
70531
|
process.on("SIGINT", async () => {
|
|
@@ -70322,89 +70539,86 @@ async function startSyncScheduler(name, options) {
|
|
|
70322
70539
|
console.log(source_default.cyan(`
|
|
70323
70540
|
=== Starting Sync Scheduler Service ===`));
|
|
70324
70541
|
console.log(source_default.cyan(`Target Server: ${serverUrl}`));
|
|
70325
|
-
|
|
70542
|
+
if (isGlobalScheduler) {
|
|
70543
|
+
console.log(source_default.cyan(`Mode: Global daemon (all enabled jobs, auto-discovers new jobs)`));
|
|
70544
|
+
console.log(source_default.cyan(`Currently registered: ${jobs.map((j) => j.name).join(", ")}
|
|
70326
70545
|
`));
|
|
70546
|
+
} else {
|
|
70547
|
+
console.log(source_default.cyan(`Active Profile: ${jobs.map((j) => j.name).join(", ")}
|
|
70548
|
+
`));
|
|
70549
|
+
}
|
|
70327
70550
|
const activeJobs = new Set;
|
|
70328
|
-
|
|
70329
|
-
|
|
70330
|
-
|
|
70331
|
-
|
|
70332
|
-
|
|
70333
|
-
console.
|
|
70334
|
-
return;
|
|
70335
|
-
}
|
|
70336
|
-
activeJobs.add(job.id);
|
|
70337
|
-
const engine = new SyncEngine(job, serverUrl, apiKey, stateDbPath);
|
|
70338
|
-
engine.on((event) => {
|
|
70339
|
-
const time = new Date().toLocaleTimeString();
|
|
70340
|
-
if (event.type === "start") {
|
|
70341
|
-
console.log(source_default.blue(`[${time}] [Sync: ${job.name}] Running scan...`));
|
|
70342
|
-
} else if (event.type === "scan-complete") {
|
|
70343
|
-
console.log(source_default.blue(`[${time}] [Sync: ${job.name}] Scan completed.`));
|
|
70344
|
-
} else if (event.type === "upload-complete") {
|
|
70345
|
-
console.log(source_default.green(`[${time}] [Sync: ${job.name}] ✔ Uploaded: ${event.path}`));
|
|
70346
|
-
} else if (event.type === "download-complete") {
|
|
70347
|
-
console.log(source_default.green(`[${time}] [Sync: ${job.name}] ✔ Downloaded: ${event.path}`));
|
|
70348
|
-
} else if (event.type === "conflict") {
|
|
70349
|
-
console.log(source_default.yellow(`[${time}] [Sync: ${job.name}] ⚠ Conflict resolved for: ${event.path}`));
|
|
70350
|
-
} else if (event.type === "error") {
|
|
70351
|
-
console.error(source_default.red(`[${time}] [Sync: ${job.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
|
|
70352
|
-
} else if (event.type === "complete") {
|
|
70353
|
-
console.log(source_default.green(`[${time}] [Sync: ${job.name}] ✔ Sync pass completed successfully.`));
|
|
70354
|
-
}
|
|
70551
|
+
const scheduledJobs = new Map;
|
|
70552
|
+
await reconcileSchedules(activeJobs, scheduledJobs, foregroundJobName);
|
|
70553
|
+
if (isGlobalScheduler) {
|
|
70554
|
+
setInterval(() => {
|
|
70555
|
+
reconcileSchedules(activeJobs, scheduledJobs).catch((reconcileErr) => {
|
|
70556
|
+
console.error(source_default.red(`[Scheduler] Failed to refresh job list: ${reconcileErr.message}`));
|
|
70355
70557
|
});
|
|
70356
|
-
|
|
70357
|
-
await engine.run();
|
|
70358
|
-
} catch (runErr) {
|
|
70359
|
-
console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${job.name}] Sync crash: ${runErr.message}`));
|
|
70360
|
-
} finally {
|
|
70361
|
-
activeJobs.delete(job.id);
|
|
70362
|
-
}
|
|
70363
|
-
};
|
|
70364
|
-
triggerSyncRun();
|
|
70365
|
-
setInterval(triggerSyncRun, intervalDelay);
|
|
70558
|
+
}, RECONCILE_INTERVAL_MS);
|
|
70366
70559
|
}
|
|
70367
70560
|
} catch (err) {
|
|
70368
70561
|
console.error(source_default.red(`Scheduler crashed: ${err.message}`));
|
|
70369
70562
|
process.exit(1);
|
|
70370
70563
|
}
|
|
70371
70564
|
}
|
|
70565
|
+
async function showSyncStatus() {
|
|
70566
|
+
try {
|
|
70567
|
+
const globalFile = path7.join(getHsyncDir(), "daemon.pid");
|
|
70568
|
+
const globalDaemon = await readPidRecord(globalFile);
|
|
70569
|
+
const config = await loadConfig();
|
|
70570
|
+
const enabledJobs = (config.syncJobs || []).filter((j) => j.enabled);
|
|
70571
|
+
if (!globalDaemon) {
|
|
70572
|
+
console.log(source_default.yellow("Global sync daemon is not running."));
|
|
70573
|
+
console.log(source_default.gray("Start it with: hsync sync start --daemon"));
|
|
70574
|
+
if (enabledJobs.length > 0) {
|
|
70575
|
+
console.log(source_default.gray(`Registered jobs waiting: ${enabledJobs.map((j) => j.name).join(", ")}`));
|
|
70576
|
+
}
|
|
70577
|
+
return;
|
|
70578
|
+
}
|
|
70579
|
+
console.log(source_default.cyan(`
|
|
70580
|
+
=== Global Sync Daemon ===`));
|
|
70581
|
+
console.log(`Status: ${source_default.green("running")} (PID: ${globalDaemon.pid})`);
|
|
70582
|
+
if (enabledJobs.length === 0) {
|
|
70583
|
+
console.log(source_default.yellow("No enabled sync jobs registered yet."));
|
|
70584
|
+
} else {
|
|
70585
|
+
console.log(`Jobs: ${enabledJobs.map((j) => j.name).join(", ")}`);
|
|
70586
|
+
console.log(source_default.gray("New jobs are picked up automatically within ~10 seconds."));
|
|
70587
|
+
}
|
|
70588
|
+
console.log();
|
|
70589
|
+
} catch (err) {
|
|
70590
|
+
console.error(source_default.red(`Error checking sync status: ${err.message}`));
|
|
70591
|
+
}
|
|
70592
|
+
}
|
|
70372
70593
|
async function stopSyncScheduler(name) {
|
|
70373
70594
|
try {
|
|
70374
|
-
const hsyncDir = path7.join(os4.homedir(), ".hsync");
|
|
70375
70595
|
if (name) {
|
|
70376
|
-
|
|
70596
|
+
console.log(source_default.yellow(`The sync daemon manages all jobs together. Stopping global daemon (includes "${name}")...`));
|
|
70597
|
+
}
|
|
70598
|
+
const globalFile = path7.join(getHsyncDir(), "daemon.pid");
|
|
70599
|
+
const globalDaemon = await readPidRecord(globalFile);
|
|
70600
|
+
if (globalDaemon) {
|
|
70601
|
+
process.kill(globalDaemon.pid, "SIGTERM");
|
|
70377
70602
|
try {
|
|
70378
|
-
|
|
70379
|
-
|
|
70380
|
-
|
|
70381
|
-
await fs6.unlink(pidFile);
|
|
70382
|
-
console.log(source_default.green(`✔ Sync daemon for "${name}" (PID: ${pid}) has been stopped.`));
|
|
70383
|
-
} catch {
|
|
70384
|
-
console.error(source_default.red(`Error: No active sync daemon found for "${name}".`));
|
|
70385
|
-
}
|
|
70603
|
+
await fs6.unlink(globalFile);
|
|
70604
|
+
} catch {}
|
|
70605
|
+
console.log(source_default.green(`✔ Global sync daemon stopped (PID: ${globalDaemon.pid}).`));
|
|
70386
70606
|
} else {
|
|
70387
|
-
|
|
70388
|
-
|
|
70389
|
-
|
|
70390
|
-
|
|
70391
|
-
|
|
70392
|
-
|
|
70393
|
-
|
|
70394
|
-
const pidFile = path7.join(hsyncDir, file);
|
|
70395
|
-
try {
|
|
70396
|
-
const pidText = await fs6.readFile(pidFile, "utf8");
|
|
70397
|
-
const pid = parseInt(pidText.trim(), 10);
|
|
70398
|
-
process.kill(pid, "SIGTERM");
|
|
70399
|
-
await fs6.unlink(pidFile);
|
|
70400
|
-
const jobName = file.replace("daemon-", "").replace(".pid", "");
|
|
70401
|
-
console.log(source_default.green(`✔ Stopped sync daemon (PID: ${pid}) for ${jobName === "daemon" ? "all jobs" : jobName}.`));
|
|
70402
|
-
} catch {
|
|
70403
|
-
try {
|
|
70404
|
-
await fs6.unlink(pidFile);
|
|
70405
|
-
} catch {}
|
|
70406
|
-
}
|
|
70607
|
+
console.log(source_default.yellow("Global sync daemon is not running."));
|
|
70608
|
+
console.log(source_default.gray("Check with: hsync sync status"));
|
|
70609
|
+
}
|
|
70610
|
+
const legacyDaemons = await listAllDaemonPids();
|
|
70611
|
+
for (const daemon of legacyDaemons) {
|
|
70612
|
+
if (daemon.jobName === "all jobs") {
|
|
70613
|
+
continue;
|
|
70407
70614
|
}
|
|
70615
|
+
try {
|
|
70616
|
+
process.kill(daemon.pid, "SIGTERM");
|
|
70617
|
+
} catch {}
|
|
70618
|
+
try {
|
|
70619
|
+
await fs6.unlink(daemon.pidFile);
|
|
70620
|
+
} catch {}
|
|
70621
|
+
console.log(source_default.green(`✔ Stopped legacy per-job daemon (PID: ${daemon.pid}) for ${daemon.jobName}.`));
|
|
70408
70622
|
}
|
|
70409
70623
|
} catch (err) {
|
|
70410
70624
|
console.error(source_default.red(`Error stopping daemon: ${err.message}`));
|
|
@@ -70583,6 +70797,172 @@ import { exec } from "child_process";
|
|
|
70583
70797
|
import fs7 from "fs/promises";
|
|
70584
70798
|
import path8 from "path";
|
|
70585
70799
|
import os5 from "os";
|
|
70800
|
+
function getHsyncDir2() {
|
|
70801
|
+
return path8.join(os5.homedir(), ".hsync");
|
|
70802
|
+
}
|
|
70803
|
+
function isProcessAlive2(pid) {
|
|
70804
|
+
try {
|
|
70805
|
+
process.kill(pid, 0);
|
|
70806
|
+
return true;
|
|
70807
|
+
} catch {
|
|
70808
|
+
return false;
|
|
70809
|
+
}
|
|
70810
|
+
}
|
|
70811
|
+
function jobNameFromPidFile2(filename) {
|
|
70812
|
+
if (filename === "daemon.pid") {
|
|
70813
|
+
return "all jobs";
|
|
70814
|
+
}
|
|
70815
|
+
return filename.replace("daemon-", "").replace(".pid", "");
|
|
70816
|
+
}
|
|
70817
|
+
async function readPidRecord2(pidFile) {
|
|
70818
|
+
try {
|
|
70819
|
+
const pidText = await fs7.readFile(pidFile, "utf8");
|
|
70820
|
+
const pid = parseInt(pidText.trim(), 10);
|
|
70821
|
+
if (isNaN(pid)) {
|
|
70822
|
+
await fs7.unlink(pidFile);
|
|
70823
|
+
return null;
|
|
70824
|
+
}
|
|
70825
|
+
if (!isProcessAlive2(pid)) {
|
|
70826
|
+
await fs7.unlink(pidFile);
|
|
70827
|
+
return null;
|
|
70828
|
+
}
|
|
70829
|
+
return { pid };
|
|
70830
|
+
} catch {
|
|
70831
|
+
return null;
|
|
70832
|
+
}
|
|
70833
|
+
}
|
|
70834
|
+
async function listAllDaemonPids2() {
|
|
70835
|
+
const hsyncDir = getHsyncDir2();
|
|
70836
|
+
const results = [];
|
|
70837
|
+
try {
|
|
70838
|
+
const files = await fs7.readdir(hsyncDir);
|
|
70839
|
+
for (const file of files) {
|
|
70840
|
+
if (!file.startsWith("daemon") || !file.endsWith(".pid")) {
|
|
70841
|
+
continue;
|
|
70842
|
+
}
|
|
70843
|
+
const pidFile = path8.join(hsyncDir, file);
|
|
70844
|
+
const record = await readPidRecord2(pidFile);
|
|
70845
|
+
if (record) {
|
|
70846
|
+
results.push({
|
|
70847
|
+
pidFile,
|
|
70848
|
+
pid: record.pid,
|
|
70849
|
+
jobName: jobNameFromPidFile2(file)
|
|
70850
|
+
});
|
|
70851
|
+
}
|
|
70852
|
+
}
|
|
70853
|
+
} catch {}
|
|
70854
|
+
return results;
|
|
70855
|
+
}
|
|
70856
|
+
var RECONCILE_INTERVAL_MS2 = 1e4;
|
|
70857
|
+
async function executeSyncPass2(jobId, activeJobs) {
|
|
70858
|
+
if (activeJobs.has(jobId)) {
|
|
70859
|
+
const config = await loadConfig();
|
|
70860
|
+
const busyJob = config.syncJobs?.find((j) => j.id === jobId);
|
|
70861
|
+
const busyName = busyJob?.name || jobId;
|
|
70862
|
+
console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${busyName}] Skip run — previous run still in progress.`));
|
|
70863
|
+
return;
|
|
70864
|
+
}
|
|
70865
|
+
const freshConfig = await loadConfig();
|
|
70866
|
+
const freshServerUrl = freshConfig.serverUrl;
|
|
70867
|
+
const freshApiKey = freshConfig.apiKey;
|
|
70868
|
+
const freshJob = freshConfig.syncJobs?.find((j) => j.id === jobId);
|
|
70869
|
+
if (!freshJob) {
|
|
70870
|
+
return;
|
|
70871
|
+
}
|
|
70872
|
+
if (!freshServerUrl || !freshApiKey) {
|
|
70873
|
+
console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Missing server URL or API key. Run: hsync login`));
|
|
70874
|
+
return;
|
|
70875
|
+
}
|
|
70876
|
+
if (!freshJob.enabled) {
|
|
70877
|
+
return;
|
|
70878
|
+
}
|
|
70879
|
+
activeJobs.add(jobId);
|
|
70880
|
+
const stateDbPath = path8.join(getHsyncDir2(), "state", `${freshJob.id}.db`);
|
|
70881
|
+
const engine = new SyncEngine(freshJob, freshServerUrl, freshApiKey, stateDbPath);
|
|
70882
|
+
let uploadCount = 0;
|
|
70883
|
+
let downloadCount = 0;
|
|
70884
|
+
let skipCount = 0;
|
|
70885
|
+
let errorCount = 0;
|
|
70886
|
+
engine.on((event) => {
|
|
70887
|
+
const time = new Date().toLocaleTimeString();
|
|
70888
|
+
if (event.type === "start") {
|
|
70889
|
+
console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Running scan...`));
|
|
70890
|
+
} else if (event.type === "scan-complete") {
|
|
70891
|
+
console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Scan completed.`));
|
|
70892
|
+
} else if (event.type === "upload-complete") {
|
|
70893
|
+
uploadCount++;
|
|
70894
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Uploaded: ${event.path}`));
|
|
70895
|
+
} else if (event.type === "download-complete") {
|
|
70896
|
+
downloadCount++;
|
|
70897
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Downloaded: ${event.path}`));
|
|
70898
|
+
} else if (event.type === "skip") {
|
|
70899
|
+
skipCount++;
|
|
70900
|
+
} else if (event.type === "conflict") {
|
|
70901
|
+
console.log(source_default.yellow(`[${time}] [Sync: ${freshJob.name}] ⚠ Conflict resolved for: ${event.path}`));
|
|
70902
|
+
} else if (event.type === "error") {
|
|
70903
|
+
errorCount++;
|
|
70904
|
+
console.error(source_default.red(`[${time}] [Sync: ${freshJob.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
|
|
70905
|
+
} else if (event.type === "complete") {
|
|
70906
|
+
const summary = `${uploadCount} uploaded, ${downloadCount} downloaded, ${skipCount} unchanged`;
|
|
70907
|
+
if (uploadCount === 0 && downloadCount === 0 && errorCount === 0) {
|
|
70908
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed (${summary}).`));
|
|
70909
|
+
} else {
|
|
70910
|
+
console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed successfully (${summary}).`));
|
|
70911
|
+
}
|
|
70912
|
+
}
|
|
70913
|
+
});
|
|
70914
|
+
try {
|
|
70915
|
+
await engine.run();
|
|
70916
|
+
} catch (runErr) {
|
|
70917
|
+
console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Sync crash: ${runErr.message}`));
|
|
70918
|
+
} finally {
|
|
70919
|
+
activeJobs.delete(jobId);
|
|
70920
|
+
}
|
|
70921
|
+
}
|
|
70922
|
+
function scheduleSyncJob2(job, activeJobs, scheduledJobs) {
|
|
70923
|
+
const intervalDelay = job.intervalMs || 1e4;
|
|
70924
|
+
const triggerSyncRun = async () => {
|
|
70925
|
+
await executeSyncPass2(job.id, activeJobs);
|
|
70926
|
+
};
|
|
70927
|
+
triggerSyncRun();
|
|
70928
|
+
const intervalId = setInterval(triggerSyncRun, intervalDelay);
|
|
70929
|
+
scheduledJobs.set(job.id, { intervalId, intervalMs: intervalDelay });
|
|
70930
|
+
console.log(source_default.green(`[${new Date().toLocaleTimeString()}] [Scheduler] Registered: ${job.name} (every ${intervalDelay / 1000}s)`));
|
|
70931
|
+
}
|
|
70932
|
+
function unscheduleSyncJob2(jobId, jobName, scheduledJobs) {
|
|
70933
|
+
const entry = scheduledJobs.get(jobId);
|
|
70934
|
+
if (!entry) {
|
|
70935
|
+
return;
|
|
70936
|
+
}
|
|
70937
|
+
clearInterval(entry.intervalId);
|
|
70938
|
+
scheduledJobs.delete(jobId);
|
|
70939
|
+
console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Scheduler] Unregistered: ${jobName || jobId}`));
|
|
70940
|
+
}
|
|
70941
|
+
async function reconcileSchedules2(activeJobs, scheduledJobs, singleJobName) {
|
|
70942
|
+
const config = await loadConfig();
|
|
70943
|
+
let jobs = (config.syncJobs || []).filter((j) => j.enabled);
|
|
70944
|
+
if (singleJobName) {
|
|
70945
|
+
jobs = jobs.filter((j) => j.name === singleJobName);
|
|
70946
|
+
}
|
|
70947
|
+
const enabledIds = new Set(jobs.map((j) => j.id));
|
|
70948
|
+
for (const [jobId, entry] of scheduledJobs) {
|
|
70949
|
+
if (!enabledIds.has(jobId)) {
|
|
70950
|
+
const staleJob = config.syncJobs?.find((j) => j.id === jobId);
|
|
70951
|
+
unscheduleSyncJob2(jobId, staleJob?.name, scheduledJobs);
|
|
70952
|
+
continue;
|
|
70953
|
+
}
|
|
70954
|
+
const job = jobs.find((j) => j.id === jobId);
|
|
70955
|
+
if (job && entry.intervalMs !== (job.intervalMs || 1e4)) {
|
|
70956
|
+
unscheduleSyncJob2(jobId, job.name, scheduledJobs);
|
|
70957
|
+
scheduleSyncJob2(job, activeJobs, scheduledJobs);
|
|
70958
|
+
}
|
|
70959
|
+
}
|
|
70960
|
+
for (const job of jobs) {
|
|
70961
|
+
if (!scheduledJobs.has(job.id)) {
|
|
70962
|
+
scheduleSyncJob2(job, activeJobs, scheduledJobs);
|
|
70963
|
+
}
|
|
70964
|
+
}
|
|
70965
|
+
}
|
|
70586
70966
|
function parseRemoteTarget3(target) {
|
|
70587
70967
|
const parts = target.split(":");
|
|
70588
70968
|
const slug = parts[0];
|
|
@@ -70591,37 +70971,44 @@ function parseRemoteTarget3(target) {
|
|
|
70591
70971
|
}
|
|
70592
70972
|
async function startSyncScheduler2(name, options) {
|
|
70593
70973
|
const isDaemon = options?.daemon;
|
|
70594
|
-
|
|
70595
|
-
const
|
|
70974
|
+
let foregroundJobName = name;
|
|
70975
|
+
const hsyncDir = getHsyncDir2();
|
|
70976
|
+
const globalPidFile = path8.join(hsyncDir, "daemon.pid");
|
|
70977
|
+
if (isDaemon && foregroundJobName) {
|
|
70978
|
+
console.log(source_default.yellow(`The sync daemon manages all enabled jobs. Starting global daemon instead of "${foregroundJobName}" only.`));
|
|
70979
|
+
foregroundJobName = undefined;
|
|
70980
|
+
}
|
|
70596
70981
|
if (isDaemon) {
|
|
70597
70982
|
await fs7.mkdir(hsyncDir, { recursive: true });
|
|
70598
|
-
|
|
70599
|
-
|
|
70600
|
-
|
|
70601
|
-
|
|
70602
|
-
|
|
70603
|
-
|
|
70604
|
-
|
|
70605
|
-
|
|
70606
|
-
|
|
70607
|
-
|
|
70608
|
-
}
|
|
70609
|
-
const logFile = path8.join(hsyncDir,
|
|
70610
|
-
console.log(source_default.cyan(`⚙️ Starting sync daemon in the background...`));
|
|
70611
|
-
const
|
|
70612
|
-
const
|
|
70613
|
-
const
|
|
70983
|
+
const existing = await readPidRecord2(globalPidFile);
|
|
70984
|
+
if (existing) {
|
|
70985
|
+
console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${existing.pid}).`));
|
|
70986
|
+
console.log(source_default.gray("New sync jobs are picked up automatically — no restart needed."));
|
|
70987
|
+
return;
|
|
70988
|
+
}
|
|
70989
|
+
const legacyDaemons = await listAllDaemonPids2();
|
|
70990
|
+
const perJobDaemons = legacyDaemons.filter((d) => d.jobName !== "all jobs");
|
|
70991
|
+
if (perJobDaemons.length > 0) {
|
|
70992
|
+
console.log(source_default.yellow("Warning: Legacy per-job daemons are still running. Stop them with: hsync sync stop"));
|
|
70993
|
+
}
|
|
70994
|
+
const logFile = path8.join(hsyncDir, "daemon.log");
|
|
70995
|
+
console.log(source_default.cyan(`⚙️ Starting global sync daemon in the background...`));
|
|
70996
|
+
const bunExec = process.execPath;
|
|
70997
|
+
const scriptPath = Bun.main;
|
|
70998
|
+
const spawnArgs = ["sync", "start"];
|
|
70999
|
+
const logFileHandle = await fs7.open(logFile, "a");
|
|
71000
|
+
const subprocess = Bun.spawn([bunExec, scriptPath, ...spawnArgs], {
|
|
70614
71001
|
detached: true,
|
|
70615
71002
|
stdout: logFileHandle.fd,
|
|
70616
71003
|
stderr: logFileHandle.fd,
|
|
70617
71004
|
stdin: "ignore"
|
|
70618
71005
|
});
|
|
70619
|
-
const pid = subprocess.pid;
|
|
70620
|
-
await fs7.writeFile(pidFile, pid.toString(), "utf8");
|
|
70621
71006
|
subprocess.unref();
|
|
70622
|
-
|
|
70623
|
-
console.log(
|
|
70624
|
-
console.log(
|
|
71007
|
+
await logFileHandle.close();
|
|
71008
|
+
console.log(source_default.green(`✔ Global sync daemon started successfully!`));
|
|
71009
|
+
console.log(source_default.gray("All enabled sync jobs will be scheduled automatically."));
|
|
71010
|
+
console.log(`Logs: hsync sync logs -f`);
|
|
71011
|
+
console.log(source_default.gray("Stop with: hsync sync stop"));
|
|
70625
71012
|
return;
|
|
70626
71013
|
}
|
|
70627
71014
|
try {
|
|
@@ -70632,11 +71019,12 @@ async function startSyncScheduler2(name, options) {
|
|
|
70632
71019
|
console.error(source_default.red("Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login"));
|
|
70633
71020
|
process.exit(1);
|
|
70634
71021
|
}
|
|
71022
|
+
const isGlobalScheduler = !foregroundJobName;
|
|
70635
71023
|
let jobs = config.syncJobs || [];
|
|
70636
|
-
if (
|
|
70637
|
-
jobs = jobs.filter((j) => j.name ===
|
|
71024
|
+
if (foregroundJobName) {
|
|
71025
|
+
jobs = jobs.filter((j) => j.name === foregroundJobName);
|
|
70638
71026
|
if (jobs.length === 0) {
|
|
70639
|
-
console.error(source_default.red(`Error: Sync job named "${
|
|
71027
|
+
console.error(source_default.red(`Error: Sync job named "${foregroundJobName}" was not found or is disabled.`));
|
|
70640
71028
|
process.exit(1);
|
|
70641
71029
|
}
|
|
70642
71030
|
} else {
|
|
@@ -70647,10 +71035,10 @@ async function startSyncScheduler2(name, options) {
|
|
|
70647
71035
|
return;
|
|
70648
71036
|
}
|
|
70649
71037
|
await fs7.mkdir(hsyncDir, { recursive: true });
|
|
70650
|
-
await fs7.writeFile(
|
|
71038
|
+
await fs7.writeFile(globalPidFile, process.pid.toString(), "utf8");
|
|
70651
71039
|
const cleanPidFile = async () => {
|
|
70652
71040
|
try {
|
|
70653
|
-
await fs7.unlink(
|
|
71041
|
+
await fs7.unlink(globalPidFile);
|
|
70654
71042
|
} catch {}
|
|
70655
71043
|
};
|
|
70656
71044
|
process.on("SIGINT", async () => {
|
|
@@ -70664,47 +71052,23 @@ async function startSyncScheduler2(name, options) {
|
|
|
70664
71052
|
console.log(source_default.cyan(`
|
|
70665
71053
|
=== Starting Sync Scheduler Service ===`));
|
|
70666
71054
|
console.log(source_default.cyan(`Target Server: ${serverUrl}`));
|
|
70667
|
-
|
|
71055
|
+
if (isGlobalScheduler) {
|
|
71056
|
+
console.log(source_default.cyan(`Mode: Global daemon (all enabled jobs, auto-discovers new jobs)`));
|
|
71057
|
+
console.log(source_default.cyan(`Currently registered: ${jobs.map((j) => j.name).join(", ")}
|
|
70668
71058
|
`));
|
|
71059
|
+
} else {
|
|
71060
|
+
console.log(source_default.cyan(`Active Profile: ${jobs.map((j) => j.name).join(", ")}
|
|
71061
|
+
`));
|
|
71062
|
+
}
|
|
70669
71063
|
const activeJobs = new Set;
|
|
70670
|
-
|
|
70671
|
-
|
|
70672
|
-
|
|
70673
|
-
|
|
70674
|
-
|
|
70675
|
-
console.
|
|
70676
|
-
return;
|
|
70677
|
-
}
|
|
70678
|
-
activeJobs.add(job.id);
|
|
70679
|
-
const engine = new SyncEngine(job, serverUrl, apiKey, stateDbPath);
|
|
70680
|
-
engine.on((event) => {
|
|
70681
|
-
const time = new Date().toLocaleTimeString();
|
|
70682
|
-
if (event.type === "start") {
|
|
70683
|
-
console.log(source_default.blue(`[${time}] [Sync: ${job.name}] Running scan...`));
|
|
70684
|
-
} else if (event.type === "scan-complete") {
|
|
70685
|
-
console.log(source_default.blue(`[${time}] [Sync: ${job.name}] Scan completed.`));
|
|
70686
|
-
} else if (event.type === "upload-complete") {
|
|
70687
|
-
console.log(source_default.green(`[${time}] [Sync: ${job.name}] ✔ Uploaded: ${event.path}`));
|
|
70688
|
-
} else if (event.type === "download-complete") {
|
|
70689
|
-
console.log(source_default.green(`[${time}] [Sync: ${job.name}] ✔ Downloaded: ${event.path}`));
|
|
70690
|
-
} else if (event.type === "conflict") {
|
|
70691
|
-
console.log(source_default.yellow(`[${time}] [Sync: ${job.name}] ⚠ Conflict resolved for: ${event.path}`));
|
|
70692
|
-
} else if (event.type === "error") {
|
|
70693
|
-
console.error(source_default.red(`[${time}] [Sync: ${job.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
|
|
70694
|
-
} else if (event.type === "complete") {
|
|
70695
|
-
console.log(source_default.green(`[${time}] [Sync: ${job.name}] ✔ Sync pass completed successfully.`));
|
|
70696
|
-
}
|
|
71064
|
+
const scheduledJobs = new Map;
|
|
71065
|
+
await reconcileSchedules2(activeJobs, scheduledJobs, foregroundJobName);
|
|
71066
|
+
if (isGlobalScheduler) {
|
|
71067
|
+
setInterval(() => {
|
|
71068
|
+
reconcileSchedules2(activeJobs, scheduledJobs).catch((reconcileErr) => {
|
|
71069
|
+
console.error(source_default.red(`[Scheduler] Failed to refresh job list: ${reconcileErr.message}`));
|
|
70697
71070
|
});
|
|
70698
|
-
|
|
70699
|
-
await engine.run();
|
|
70700
|
-
} catch (runErr) {
|
|
70701
|
-
console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${job.name}] Sync crash: ${runErr.message}`));
|
|
70702
|
-
} finally {
|
|
70703
|
-
activeJobs.delete(job.id);
|
|
70704
|
-
}
|
|
70705
|
-
};
|
|
70706
|
-
triggerSyncRun();
|
|
70707
|
-
setInterval(triggerSyncRun, intervalDelay);
|
|
71071
|
+
}, RECONCILE_INTERVAL_MS2);
|
|
70708
71072
|
}
|
|
70709
71073
|
} catch (err) {
|
|
70710
71074
|
console.error(source_default.red(`Scheduler crashed: ${err.message}`));
|
|
@@ -70713,40 +71077,33 @@ async function startSyncScheduler2(name, options) {
|
|
|
70713
71077
|
}
|
|
70714
71078
|
async function stopSyncScheduler2(name) {
|
|
70715
71079
|
try {
|
|
70716
|
-
const hsyncDir = path8.join(os5.homedir(), ".hsync");
|
|
70717
71080
|
if (name) {
|
|
70718
|
-
|
|
71081
|
+
console.log(source_default.yellow(`The sync daemon manages all jobs together. Stopping global daemon (includes "${name}")...`));
|
|
71082
|
+
}
|
|
71083
|
+
const globalFile = path8.join(getHsyncDir2(), "daemon.pid");
|
|
71084
|
+
const globalDaemon = await readPidRecord2(globalFile);
|
|
71085
|
+
if (globalDaemon) {
|
|
71086
|
+
process.kill(globalDaemon.pid, "SIGTERM");
|
|
70719
71087
|
try {
|
|
70720
|
-
|
|
70721
|
-
|
|
70722
|
-
|
|
70723
|
-
await fs7.unlink(pidFile);
|
|
70724
|
-
console.log(source_default.green(`✔ Sync daemon for "${name}" (PID: ${pid}) has been stopped.`));
|
|
70725
|
-
} catch {
|
|
70726
|
-
console.error(source_default.red(`Error: No active sync daemon found for "${name}".`));
|
|
70727
|
-
}
|
|
71088
|
+
await fs7.unlink(globalFile);
|
|
71089
|
+
} catch {}
|
|
71090
|
+
console.log(source_default.green(`✔ Global sync daemon stopped (PID: ${globalDaemon.pid}).`));
|
|
70728
71091
|
} else {
|
|
70729
|
-
|
|
70730
|
-
|
|
70731
|
-
|
|
70732
|
-
|
|
70733
|
-
|
|
70734
|
-
|
|
70735
|
-
|
|
70736
|
-
const pidFile = path8.join(hsyncDir, file);
|
|
70737
|
-
try {
|
|
70738
|
-
const pidText = await fs7.readFile(pidFile, "utf8");
|
|
70739
|
-
const pid = parseInt(pidText.trim(), 10);
|
|
70740
|
-
process.kill(pid, "SIGTERM");
|
|
70741
|
-
await fs7.unlink(pidFile);
|
|
70742
|
-
const jobName = file.replace("daemon-", "").replace(".pid", "");
|
|
70743
|
-
console.log(source_default.green(`✔ Stopped sync daemon (PID: ${pid}) for ${jobName === "daemon" ? "all jobs" : jobName}.`));
|
|
70744
|
-
} catch {
|
|
70745
|
-
try {
|
|
70746
|
-
await fs7.unlink(pidFile);
|
|
70747
|
-
} catch {}
|
|
70748
|
-
}
|
|
71092
|
+
console.log(source_default.yellow("Global sync daemon is not running."));
|
|
71093
|
+
console.log(source_default.gray("Check with: hsync sync status"));
|
|
71094
|
+
}
|
|
71095
|
+
const legacyDaemons = await listAllDaemonPids2();
|
|
71096
|
+
for (const daemon of legacyDaemons) {
|
|
71097
|
+
if (daemon.jobName === "all jobs") {
|
|
71098
|
+
continue;
|
|
70749
71099
|
}
|
|
71100
|
+
try {
|
|
71101
|
+
process.kill(daemon.pid, "SIGTERM");
|
|
71102
|
+
} catch {}
|
|
71103
|
+
try {
|
|
71104
|
+
await fs7.unlink(daemon.pidFile);
|
|
71105
|
+
} catch {}
|
|
71106
|
+
console.log(source_default.green(`✔ Stopped legacy per-job daemon (PID: ${daemon.pid}) for ${daemon.jobName}.`));
|
|
70750
71107
|
}
|
|
70751
71108
|
} catch (err) {
|
|
70752
71109
|
console.error(source_default.red(`Error stopping daemon: ${err.message}`));
|
|
@@ -70866,7 +71223,7 @@ async function editSyncJob2(name, options) {
|
|
|
70866
71223
|
}
|
|
70867
71224
|
|
|
70868
71225
|
// src/commands/ui.ts
|
|
70869
|
-
var __dirname = "/
|
|
71226
|
+
var __dirname = "/home/hamid/packages/node/hmd/file-sync/packages/cli/src/commands";
|
|
70870
71227
|
function openBrowser(url2) {
|
|
70871
71228
|
const start = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
70872
71229
|
exec(`${start} ${url2}`).unref();
|
|
@@ -70914,29 +71271,22 @@ async function startUiServer(options) {
|
|
|
70914
71271
|
});
|
|
70915
71272
|
localRouter.get("/jobs/:name/status", async (req, res) => {
|
|
70916
71273
|
try {
|
|
70917
|
-
const
|
|
70918
|
-
const hsyncDir = path9.join(os6.homedir(), ".hsync");
|
|
70919
|
-
const pidFile = path9.join(hsyncDir, `daemon-${jobName}.pid`);
|
|
70920
|
-
const globalPidFile = path9.join(hsyncDir, "daemon.pid");
|
|
71274
|
+
const globalPidFile = path9.join(os6.homedir(), ".hsync", "daemon.pid");
|
|
70921
71275
|
let pidText = "";
|
|
70922
71276
|
try {
|
|
70923
|
-
pidText = await fs8.readFile(
|
|
71277
|
+
pidText = await fs8.readFile(globalPidFile, "utf8");
|
|
70924
71278
|
} catch {
|
|
70925
|
-
|
|
70926
|
-
pidText = await fs8.readFile(globalPidFile, "utf8");
|
|
70927
|
-
} catch {
|
|
70928
|
-
return res.json({ running: false });
|
|
70929
|
-
}
|
|
71279
|
+
return res.json({ running: false, mode: "global" });
|
|
70930
71280
|
}
|
|
70931
71281
|
const pid = parseInt(pidText.trim(), 10);
|
|
70932
71282
|
if (isNaN(pid)) {
|
|
70933
|
-
return res.json({ running: false });
|
|
71283
|
+
return res.json({ running: false, mode: "global" });
|
|
70934
71284
|
}
|
|
70935
71285
|
try {
|
|
70936
71286
|
process.kill(pid, 0);
|
|
70937
|
-
res.json({ running: true, pid });
|
|
71287
|
+
res.json({ running: true, pid, mode: "global" });
|
|
70938
71288
|
} catch {
|
|
70939
|
-
res.json({ running: false });
|
|
71289
|
+
res.json({ running: false, mode: "global" });
|
|
70940
71290
|
}
|
|
70941
71291
|
} catch (err) {
|
|
70942
71292
|
res.status(500).json({ error: err.message });
|
|
@@ -70944,18 +71294,16 @@ async function startUiServer(options) {
|
|
|
70944
71294
|
});
|
|
70945
71295
|
localRouter.post("/jobs/:name/start", async (req, res) => {
|
|
70946
71296
|
try {
|
|
70947
|
-
|
|
70948
|
-
|
|
70949
|
-
res.json({ success: true });
|
|
71297
|
+
await startSyncScheduler2(undefined, { daemon: true });
|
|
71298
|
+
res.json({ success: true, mode: "global" });
|
|
70950
71299
|
} catch (err) {
|
|
70951
71300
|
res.status(500).json({ error: err.message });
|
|
70952
71301
|
}
|
|
70953
71302
|
});
|
|
70954
71303
|
localRouter.post("/jobs/:name/stop", async (req, res) => {
|
|
70955
71304
|
try {
|
|
70956
|
-
|
|
70957
|
-
|
|
70958
|
-
res.json({ success: true });
|
|
71305
|
+
await stopSyncScheduler2();
|
|
71306
|
+
res.json({ success: true, mode: "global" });
|
|
70959
71307
|
} catch (err) {
|
|
70960
71308
|
res.status(500).json({ error: err.message });
|
|
70961
71309
|
}
|
|
@@ -71100,7 +71448,7 @@ Press Ctrl+C to stop the server...
|
|
|
71100
71448
|
// package.json
|
|
71101
71449
|
var package_default = {
|
|
71102
71450
|
name: "hsync-cli",
|
|
71103
|
-
version: "1.1.
|
|
71451
|
+
version: "1.1.3",
|
|
71104
71452
|
description: "hsync CLI for file sync operations",
|
|
71105
71453
|
bin: {
|
|
71106
71454
|
hsync: "./dist/bin.js"
|
|
@@ -71177,12 +71525,15 @@ syncCmd.command("list").description("List all registered local sync profiles").a
|
|
|
71177
71525
|
syncCmd.command("remove <name>").description("Remove a registered local sync job").action(async (name) => {
|
|
71178
71526
|
await removeSyncJob(name);
|
|
71179
71527
|
});
|
|
71180
|
-
syncCmd.command("start [name]").description("Start
|
|
71528
|
+
syncCmd.command("start [name]").description("Start sync scheduler (use --daemon to run all jobs in background)").option("-d, --daemon", "Run global sync daemon for all enabled jobs in the background").action(async (name, options) => {
|
|
71181
71529
|
await startSyncScheduler(name, options);
|
|
71182
71530
|
});
|
|
71183
|
-
syncCmd.command("stop [name]").description("Stop
|
|
71531
|
+
syncCmd.command("stop [name]").description("Stop the global background sync daemon").action(async (name) => {
|
|
71184
71532
|
await stopSyncScheduler(name);
|
|
71185
71533
|
});
|
|
71534
|
+
syncCmd.command("status").description("Show active background sync daemons").action(async () => {
|
|
71535
|
+
await showSyncStatus();
|
|
71536
|
+
});
|
|
71186
71537
|
syncCmd.command("autostart <action>").description('Configure hsync sync autostart daemon on login (action: "enable" | "disable")').action(async (action) => {
|
|
71187
71538
|
if (action === "enable") {
|
|
71188
71539
|
await configureAutostart(true);
|
package/dist/web/index.html
CHANGED
|
File without changes
|