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 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 remotePath = (parts.slice(1).join(":") || "").replace(/^\/+|\/+$/g, "");
69674
- return { slug, remotePath };
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
- const serverUrl = config.serverUrl;
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
- const prompter = new OverwritePrompter(forceYes);
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
- const confirm = await prompter.shouldOverwrite(localFile.relativePath);
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, destinationPath, localFile.absolutePath);
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
- const serverUrl = config.serverUrl;
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
- await fs3.mkdir(resolvedLocalPath, { recursive: true });
69755
- const localFiles = await scanLocalPath(resolvedLocalPath, resolvedLocalPath);
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(forceYes);
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
- const localFile = localFiles.find((f) => f.relativePath === remoteFile.path);
69763
- const localDestPath = path4.join(resolvedLocalPath, remoteFile.path);
69764
- const sourcePath = remote.remotePath ? `${remote.remotePath}/${remoteFile.path}` : remoteFile.path;
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 confirm = await prompter.shouldOverwrite(remoteFile.path);
69770
- if (confirm) {
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 fileSpinner = ora2(`Pulling ${remoteFile.path}...`).start();
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, sourcePath, localDestPath);
69782
- fileSpinner.succeed(source_default.green(`Pulled ${remoteFile.path}`));
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 ${remoteFile.path}: ${downloadErr.message}`));
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
- const hsyncDir = path7.join(os4.homedir(), ".hsync");
70253
- const pidFile = path7.join(hsyncDir, name ? `daemon-${name}.pid` : "daemon.pid");
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
- try {
70257
- const existingPidText = await fs6.readFile(pidFile, "utf8");
70258
- if (existingPidText) {
70259
- const pid2 = parseInt(existingPidText.trim(), 10);
70260
- try {
70261
- process.kill(pid2, 0);
70262
- console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${pid2}).`));
70263
- return;
70264
- } catch {}
70265
- }
70266
- } catch {}
70267
- const logFile = path7.join(hsyncDir, name ? `daemon-${name}.log` : "daemon.log");
70268
- console.log(source_default.cyan(`⚙️ Starting sync daemon in the background...`));
70269
- const filteredArgs = process.argv.slice(1).filter((arg) => arg !== "--daemon" && arg !== "-d");
70270
- const logFileHandle = await fs6.open(logFile, "w");
70271
- const subprocess = Bun.spawn([process.argv[0], ...filteredArgs], {
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
- console.log(source_default.green(`✔ Sync daemon started successfully!`));
70281
- console.log(`PID: ${pid}`);
70282
- console.log(`Logs: tail -f ${logFile}`);
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 (name) {
70295
- jobs = jobs.filter((j) => j.name === 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 "${name}" was not found or is disabled.`));
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(pidFile, process.pid.toString(), "utf8");
70525
+ await fs6.writeFile(globalPidFile, process.pid.toString(), "utf8");
70309
70526
  const cleanPidFile = async () => {
70310
70527
  try {
70311
- await fs6.unlink(pidFile);
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
- console.log(source_default.cyan(`Active Profiles: ${jobs.map((j) => j.name).join(", ")}
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
- for (const job of jobs) {
70329
- const intervalDelay = job.intervalMs || 1e4;
70330
- const stateDbPath = path7.join(os4.homedir(), ".hsync", "state", `${job.id}.db`);
70331
- const triggerSyncRun = async () => {
70332
- if (activeJobs.has(job.id)) {
70333
- console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${job.name}] Skip run — previous run still in progress.`));
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
- try {
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
- const pidFile = path7.join(hsyncDir, `daemon-${name}.pid`);
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
- const pidText = await fs6.readFile(pidFile, "utf8");
70379
- const pid = parseInt(pidText.trim(), 10);
70380
- process.kill(pid, "SIGTERM");
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
- const files = await fs6.readdir(hsyncDir);
70388
- const pidFiles = files.filter((f) => f.startsWith("daemon") && f.endsWith(".pid"));
70389
- if (pidFiles.length === 0) {
70390
- console.log(source_default.yellow("No active sync daemons found running."));
70391
- return;
70392
- }
70393
- for (const file of pidFiles) {
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
- const hsyncDir = path8.join(os5.homedir(), ".hsync");
70595
- const pidFile = path8.join(hsyncDir, name ? `daemon-${name}.pid` : "daemon.pid");
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
- try {
70599
- const existingPidText = await fs7.readFile(pidFile, "utf8");
70600
- if (existingPidText) {
70601
- const pid2 = parseInt(existingPidText.trim(), 10);
70602
- try {
70603
- process.kill(pid2, 0);
70604
- console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${pid2}).`));
70605
- return;
70606
- } catch {}
70607
- }
70608
- } catch {}
70609
- const logFile = path8.join(hsyncDir, name ? `daemon-${name}.log` : "daemon.log");
70610
- console.log(source_default.cyan(`⚙️ Starting sync daemon in the background...`));
70611
- const filteredArgs = process.argv.slice(1).filter((arg) => arg !== "--daemon" && arg !== "-d");
70612
- const logFileHandle = await fs7.open(logFile, "w");
70613
- const subprocess = Bun.spawn([process.argv[0], ...filteredArgs], {
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
- console.log(source_default.green(`✔ Sync daemon started successfully!`));
70623
- console.log(`PID: ${pid}`);
70624
- console.log(`Logs: tail -f ${logFile}`);
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 (name) {
70637
- jobs = jobs.filter((j) => j.name === 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 "${name}" was not found or is disabled.`));
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(pidFile, process.pid.toString(), "utf8");
71038
+ await fs7.writeFile(globalPidFile, process.pid.toString(), "utf8");
70651
71039
  const cleanPidFile = async () => {
70652
71040
  try {
70653
- await fs7.unlink(pidFile);
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
- console.log(source_default.cyan(`Active Profiles: ${jobs.map((j) => j.name).join(", ")}
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
- for (const job of jobs) {
70671
- const intervalDelay = job.intervalMs || 1e4;
70672
- const stateDbPath = path8.join(os5.homedir(), ".hsync", "state", `${job.id}.db`);
70673
- const triggerSyncRun = async () => {
70674
- if (activeJobs.has(job.id)) {
70675
- console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${job.name}] Skip run — previous run still in progress.`));
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
- try {
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
- const pidFile = path8.join(hsyncDir, `daemon-${name}.pid`);
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
- const pidText = await fs7.readFile(pidFile, "utf8");
70721
- const pid = parseInt(pidText.trim(), 10);
70722
- process.kill(pid, "SIGTERM");
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
- const files = await fs7.readdir(hsyncDir);
70730
- const pidFiles = files.filter((f) => f.startsWith("daemon") && f.endsWith(".pid"));
70731
- if (pidFiles.length === 0) {
70732
- console.log(source_default.yellow("No active sync daemons found running."));
70733
- return;
70734
- }
70735
- for (const file of pidFiles) {
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 = "/media/hyper2/HYPER/projects/node/hmd/file-sync-v2/packages/cli/src/commands";
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 jobName = req.params.name;
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(pidFile, "utf8");
71277
+ pidText = await fs8.readFile(globalPidFile, "utf8");
70924
71278
  } catch {
70925
- try {
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
- const jobName = req.params.name;
70948
- await startSyncScheduler2(jobName, { daemon: true });
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
- const jobName = req.params.name;
70957
- await stopSyncScheduler2(jobName);
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.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 interval synchronization scheduler service").option("-d, --daemon", "Run synchronization service in the background (detached daemon mode)").action(async (name, options) => {
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 active background synchronization scheduler daemon").action(async (name) => {
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);
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hsync-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "hsync CLI for file sync operations",
5
5
  "bin": {
6
6
  "hsync": "./dist/bin.js"