hsync-cli 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +532 -214
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -70153,6 +70153,177 @@ class SyncEngine {
70153
70153
  }
70154
70154
  }
70155
70155
  // src/commands/sync.ts
70156
+ function getHsyncDir() {
70157
+ return path7.join(os4.homedir(), ".hsync");
70158
+ }
70159
+ function isProcessAlive(pid) {
70160
+ try {
70161
+ process.kill(pid, 0);
70162
+ return true;
70163
+ } catch {
70164
+ return false;
70165
+ }
70166
+ }
70167
+ function jobNameFromPidFile(filename) {
70168
+ if (filename === "daemon.pid") {
70169
+ return "all jobs";
70170
+ }
70171
+ return filename.replace("daemon-", "").replace(".pid", "");
70172
+ }
70173
+ async function readPidRecord(pidFile) {
70174
+ try {
70175
+ const pidText = await fs6.readFile(pidFile, "utf8");
70176
+ const pid = parseInt(pidText.trim(), 10);
70177
+ if (isNaN(pid)) {
70178
+ await fs6.unlink(pidFile);
70179
+ return null;
70180
+ }
70181
+ if (!isProcessAlive(pid)) {
70182
+ await fs6.unlink(pidFile);
70183
+ return null;
70184
+ }
70185
+ return { pid };
70186
+ } catch {
70187
+ return null;
70188
+ }
70189
+ }
70190
+ async function listAllDaemonPids() {
70191
+ const hsyncDir = getHsyncDir();
70192
+ const results = [];
70193
+ try {
70194
+ const files = await fs6.readdir(hsyncDir);
70195
+ for (const file of files) {
70196
+ if (!file.startsWith("daemon") || !file.endsWith(".pid")) {
70197
+ continue;
70198
+ }
70199
+ const pidFile = path7.join(hsyncDir, file);
70200
+ const record = await readPidRecord(pidFile);
70201
+ if (record) {
70202
+ results.push({
70203
+ pidFile,
70204
+ pid: record.pid,
70205
+ jobName: jobNameFromPidFile(file)
70206
+ });
70207
+ }
70208
+ }
70209
+ } catch {}
70210
+ return results;
70211
+ }
70212
+ var RECONCILE_INTERVAL_MS = 1e4;
70213
+ async function isGlobalDaemonRunning() {
70214
+ const globalFile = path7.join(getHsyncDir(), "daemon.pid");
70215
+ const record = await readPidRecord(globalFile);
70216
+ return record !== null;
70217
+ }
70218
+ async function executeSyncPass(jobId, activeJobs) {
70219
+ if (activeJobs.has(jobId)) {
70220
+ const config = await loadConfig();
70221
+ const busyJob = config.syncJobs?.find((j) => j.id === jobId);
70222
+ const busyName = busyJob?.name || jobId;
70223
+ console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${busyName}] Skip run — previous run still in progress.`));
70224
+ return;
70225
+ }
70226
+ const freshConfig = await loadConfig();
70227
+ const freshServerUrl = freshConfig.serverUrl;
70228
+ const freshApiKey = freshConfig.apiKey;
70229
+ const freshJob = freshConfig.syncJobs?.find((j) => j.id === jobId);
70230
+ if (!freshJob) {
70231
+ return;
70232
+ }
70233
+ if (!freshServerUrl || !freshApiKey) {
70234
+ console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Missing server URL or API key. Run: hsync login`));
70235
+ return;
70236
+ }
70237
+ if (!freshJob.enabled) {
70238
+ return;
70239
+ }
70240
+ activeJobs.add(jobId);
70241
+ const stateDbPath = path7.join(getHsyncDir(), "state", `${freshJob.id}.db`);
70242
+ const engine = new SyncEngine(freshJob, freshServerUrl, freshApiKey, stateDbPath);
70243
+ let uploadCount = 0;
70244
+ let downloadCount = 0;
70245
+ let skipCount = 0;
70246
+ let errorCount = 0;
70247
+ engine.on((event) => {
70248
+ const time = new Date().toLocaleTimeString();
70249
+ if (event.type === "start") {
70250
+ console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Running scan...`));
70251
+ } else if (event.type === "scan-complete") {
70252
+ console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Scan completed.`));
70253
+ } else if (event.type === "upload-complete") {
70254
+ uploadCount++;
70255
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Uploaded: ${event.path}`));
70256
+ } else if (event.type === "download-complete") {
70257
+ downloadCount++;
70258
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Downloaded: ${event.path}`));
70259
+ } else if (event.type === "skip") {
70260
+ skipCount++;
70261
+ } else if (event.type === "conflict") {
70262
+ console.log(source_default.yellow(`[${time}] [Sync: ${freshJob.name}] ⚠ Conflict resolved for: ${event.path}`));
70263
+ } else if (event.type === "error") {
70264
+ errorCount++;
70265
+ console.error(source_default.red(`[${time}] [Sync: ${freshJob.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
70266
+ } else if (event.type === "complete") {
70267
+ const summary = `${uploadCount} uploaded, ${downloadCount} downloaded, ${skipCount} unchanged`;
70268
+ if (uploadCount === 0 && downloadCount === 0 && errorCount === 0) {
70269
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed (${summary}).`));
70270
+ } else {
70271
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed successfully (${summary}).`));
70272
+ }
70273
+ }
70274
+ });
70275
+ try {
70276
+ await engine.run();
70277
+ } catch (runErr) {
70278
+ console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Sync crash: ${runErr.message}`));
70279
+ } finally {
70280
+ activeJobs.delete(jobId);
70281
+ }
70282
+ }
70283
+ function scheduleSyncJob(job, activeJobs, scheduledJobs) {
70284
+ const intervalDelay = job.intervalMs || 1e4;
70285
+ const triggerSyncRun = async () => {
70286
+ await executeSyncPass(job.id, activeJobs);
70287
+ };
70288
+ triggerSyncRun();
70289
+ const intervalId = setInterval(triggerSyncRun, intervalDelay);
70290
+ scheduledJobs.set(job.id, { intervalId, intervalMs: intervalDelay });
70291
+ console.log(source_default.green(`[${new Date().toLocaleTimeString()}] [Scheduler] Registered: ${job.name} (every ${intervalDelay / 1000}s)`));
70292
+ }
70293
+ function unscheduleSyncJob(jobId, jobName, scheduledJobs) {
70294
+ const entry = scheduledJobs.get(jobId);
70295
+ if (!entry) {
70296
+ return;
70297
+ }
70298
+ clearInterval(entry.intervalId);
70299
+ scheduledJobs.delete(jobId);
70300
+ console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Scheduler] Unregistered: ${jobName || jobId}`));
70301
+ }
70302
+ async function reconcileSchedules(activeJobs, scheduledJobs, singleJobName) {
70303
+ const config = await loadConfig();
70304
+ let jobs = (config.syncJobs || []).filter((j) => j.enabled);
70305
+ if (singleJobName) {
70306
+ jobs = jobs.filter((j) => j.name === singleJobName);
70307
+ }
70308
+ const enabledIds = new Set(jobs.map((j) => j.id));
70309
+ for (const [jobId, entry] of scheduledJobs) {
70310
+ if (!enabledIds.has(jobId)) {
70311
+ const staleJob = config.syncJobs?.find((j) => j.id === jobId);
70312
+ unscheduleSyncJob(jobId, staleJob?.name, scheduledJobs);
70313
+ continue;
70314
+ }
70315
+ const job = jobs.find((j) => j.id === jobId);
70316
+ if (job && entry.intervalMs !== (job.intervalMs || 1e4)) {
70317
+ unscheduleSyncJob(jobId, job.name, scheduledJobs);
70318
+ scheduleSyncJob(job, activeJobs, scheduledJobs);
70319
+ }
70320
+ }
70321
+ for (const job of jobs) {
70322
+ if (!scheduledJobs.has(job.id)) {
70323
+ scheduleSyncJob(job, activeJobs, scheduledJobs);
70324
+ }
70325
+ }
70326
+ }
70156
70327
  function parseRemoteTarget2(target) {
70157
70328
  const parts = target.split(":");
70158
70329
  const slug = parts[0];
@@ -70201,6 +70372,11 @@ async function addSyncJob(localPath, remoteTarget, options) {
70201
70372
  console.log(`Direction: ${newJob.direction}`);
70202
70373
  console.log(`Strategy: ${newJob.conflictStrategy}`);
70203
70374
  console.log(`Interval: ${intervalSec} seconds`);
70375
+ if (await isGlobalDaemonRunning()) {
70376
+ console.log(source_default.gray("The running sync daemon will pick up this job automatically."));
70377
+ } else {
70378
+ console.log(source_default.gray("Start all sync jobs with: hsync sync start --daemon"));
70379
+ }
70204
70380
  } catch (err) {
70205
70381
  console.error(source_default.red(`Error: Failed to register sync job: ${err.message}`));
70206
70382
  process.exit(1);
@@ -70249,37 +70425,44 @@ async function removeSyncJob(name) {
70249
70425
  }
70250
70426
  async function startSyncScheduler(name, options) {
70251
70427
  const isDaemon = options?.daemon;
70252
- const hsyncDir = path7.join(os4.homedir(), ".hsync");
70253
- const pidFile = path7.join(hsyncDir, name ? `daemon-${name}.pid` : "daemon.pid");
70428
+ let foregroundJobName = name;
70429
+ const hsyncDir = getHsyncDir();
70430
+ const globalPidFile = path7.join(hsyncDir, "daemon.pid");
70431
+ if (isDaemon && foregroundJobName) {
70432
+ console.log(source_default.yellow(`The sync daemon manages all enabled jobs. Starting global daemon instead of "${foregroundJobName}" only.`));
70433
+ foregroundJobName = undefined;
70434
+ }
70254
70435
  if (isDaemon) {
70255
70436
  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], {
70437
+ const existing = await readPidRecord(globalPidFile);
70438
+ if (existing) {
70439
+ console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${existing.pid}).`));
70440
+ console.log(source_default.gray("New sync jobs are picked up automatically — no restart needed."));
70441
+ return;
70442
+ }
70443
+ const legacyDaemons = await listAllDaemonPids();
70444
+ const perJobDaemons = legacyDaemons.filter((d) => d.jobName !== "all jobs");
70445
+ if (perJobDaemons.length > 0) {
70446
+ console.log(source_default.yellow("Warning: Legacy per-job daemons are still running. Stop them with: hsync sync stop"));
70447
+ }
70448
+ const logFile = path7.join(hsyncDir, "daemon.log");
70449
+ console.log(source_default.cyan(`⚙️ Starting global sync daemon in the background...`));
70450
+ const bunExec = process.execPath;
70451
+ const scriptPath = Bun.main;
70452
+ const spawnArgs = ["sync", "start"];
70453
+ const logFileHandle = await fs6.open(logFile, "a");
70454
+ const subprocess = Bun.spawn([bunExec, scriptPath, ...spawnArgs], {
70272
70455
  detached: true,
70273
70456
  stdout: logFileHandle.fd,
70274
70457
  stderr: logFileHandle.fd,
70275
70458
  stdin: "ignore"
70276
70459
  });
70277
- const pid = subprocess.pid;
70278
- await fs6.writeFile(pidFile, pid.toString(), "utf8");
70279
70460
  subprocess.unref();
70280
- console.log(source_default.green(`✔ Sync daemon started successfully!`));
70281
- console.log(`PID: ${pid}`);
70282
- console.log(`Logs: tail -f ${logFile}`);
70461
+ await logFileHandle.close();
70462
+ console.log(source_default.green(`✔ Global sync daemon started successfully!`));
70463
+ console.log(source_default.gray("All enabled sync jobs will be scheduled automatically."));
70464
+ console.log(`Logs: hsync sync logs -f`);
70465
+ console.log(source_default.gray("Stop with: hsync sync stop"));
70283
70466
  return;
70284
70467
  }
70285
70468
  try {
@@ -70290,11 +70473,12 @@ async function startSyncScheduler(name, options) {
70290
70473
  console.error(source_default.red("Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login"));
70291
70474
  process.exit(1);
70292
70475
  }
70476
+ const isGlobalScheduler = !foregroundJobName;
70293
70477
  let jobs = config.syncJobs || [];
70294
- if (name) {
70295
- jobs = jobs.filter((j) => j.name === name);
70478
+ if (foregroundJobName) {
70479
+ jobs = jobs.filter((j) => j.name === foregroundJobName);
70296
70480
  if (jobs.length === 0) {
70297
- console.error(source_default.red(`Error: Sync job named "${name}" was not found or is disabled.`));
70481
+ console.error(source_default.red(`Error: Sync job named "${foregroundJobName}" was not found or is disabled.`));
70298
70482
  process.exit(1);
70299
70483
  }
70300
70484
  } else {
@@ -70305,10 +70489,10 @@ async function startSyncScheduler(name, options) {
70305
70489
  return;
70306
70490
  }
70307
70491
  await fs6.mkdir(hsyncDir, { recursive: true });
70308
- await fs6.writeFile(pidFile, process.pid.toString(), "utf8");
70492
+ await fs6.writeFile(globalPidFile, process.pid.toString(), "utf8");
70309
70493
  const cleanPidFile = async () => {
70310
70494
  try {
70311
- await fs6.unlink(pidFile);
70495
+ await fs6.unlink(globalPidFile);
70312
70496
  } catch {}
70313
70497
  };
70314
70498
  process.on("SIGINT", async () => {
@@ -70322,89 +70506,86 @@ async function startSyncScheduler(name, options) {
70322
70506
  console.log(source_default.cyan(`
70323
70507
  === Starting Sync Scheduler Service ===`));
70324
70508
  console.log(source_default.cyan(`Target Server: ${serverUrl}`));
70325
- console.log(source_default.cyan(`Active Profiles: ${jobs.map((j) => j.name).join(", ")}
70509
+ if (isGlobalScheduler) {
70510
+ console.log(source_default.cyan(`Mode: Global daemon (all enabled jobs, auto-discovers new jobs)`));
70511
+ console.log(source_default.cyan(`Currently registered: ${jobs.map((j) => j.name).join(", ")}
70326
70512
  `));
70513
+ } else {
70514
+ console.log(source_default.cyan(`Active Profile: ${jobs.map((j) => j.name).join(", ")}
70515
+ `));
70516
+ }
70327
70517
  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
- }
70518
+ const scheduledJobs = new Map;
70519
+ await reconcileSchedules(activeJobs, scheduledJobs, foregroundJobName);
70520
+ if (isGlobalScheduler) {
70521
+ setInterval(() => {
70522
+ reconcileSchedules(activeJobs, scheduledJobs).catch((reconcileErr) => {
70523
+ console.error(source_default.red(`[Scheduler] Failed to refresh job list: ${reconcileErr.message}`));
70355
70524
  });
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);
70525
+ }, RECONCILE_INTERVAL_MS);
70366
70526
  }
70367
70527
  } catch (err) {
70368
70528
  console.error(source_default.red(`Scheduler crashed: ${err.message}`));
70369
70529
  process.exit(1);
70370
70530
  }
70371
70531
  }
70532
+ async function showSyncStatus() {
70533
+ try {
70534
+ const globalFile = path7.join(getHsyncDir(), "daemon.pid");
70535
+ const globalDaemon = await readPidRecord(globalFile);
70536
+ const config = await loadConfig();
70537
+ const enabledJobs = (config.syncJobs || []).filter((j) => j.enabled);
70538
+ if (!globalDaemon) {
70539
+ console.log(source_default.yellow("Global sync daemon is not running."));
70540
+ console.log(source_default.gray("Start it with: hsync sync start --daemon"));
70541
+ if (enabledJobs.length > 0) {
70542
+ console.log(source_default.gray(`Registered jobs waiting: ${enabledJobs.map((j) => j.name).join(", ")}`));
70543
+ }
70544
+ return;
70545
+ }
70546
+ console.log(source_default.cyan(`
70547
+ === Global Sync Daemon ===`));
70548
+ console.log(`Status: ${source_default.green("running")} (PID: ${globalDaemon.pid})`);
70549
+ if (enabledJobs.length === 0) {
70550
+ console.log(source_default.yellow("No enabled sync jobs registered yet."));
70551
+ } else {
70552
+ console.log(`Jobs: ${enabledJobs.map((j) => j.name).join(", ")}`);
70553
+ console.log(source_default.gray("New jobs are picked up automatically within ~10 seconds."));
70554
+ }
70555
+ console.log();
70556
+ } catch (err) {
70557
+ console.error(source_default.red(`Error checking sync status: ${err.message}`));
70558
+ }
70559
+ }
70372
70560
  async function stopSyncScheduler(name) {
70373
70561
  try {
70374
- const hsyncDir = path7.join(os4.homedir(), ".hsync");
70375
70562
  if (name) {
70376
- const pidFile = path7.join(hsyncDir, `daemon-${name}.pid`);
70563
+ console.log(source_default.yellow(`The sync daemon manages all jobs together. Stopping global daemon (includes "${name}")...`));
70564
+ }
70565
+ const globalFile = path7.join(getHsyncDir(), "daemon.pid");
70566
+ const globalDaemon = await readPidRecord(globalFile);
70567
+ if (globalDaemon) {
70568
+ process.kill(globalDaemon.pid, "SIGTERM");
70377
70569
  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
- }
70570
+ await fs6.unlink(globalFile);
70571
+ } catch {}
70572
+ console.log(source_default.green(`✔ Global sync daemon stopped (PID: ${globalDaemon.pid}).`));
70386
70573
  } 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
- }
70574
+ console.log(source_default.yellow("Global sync daemon is not running."));
70575
+ console.log(source_default.gray("Check with: hsync sync status"));
70576
+ }
70577
+ const legacyDaemons = await listAllDaemonPids();
70578
+ for (const daemon of legacyDaemons) {
70579
+ if (daemon.jobName === "all jobs") {
70580
+ continue;
70407
70581
  }
70582
+ try {
70583
+ process.kill(daemon.pid, "SIGTERM");
70584
+ } catch {}
70585
+ try {
70586
+ await fs6.unlink(daemon.pidFile);
70587
+ } catch {}
70588
+ console.log(source_default.green(`✔ Stopped legacy per-job daemon (PID: ${daemon.pid}) for ${daemon.jobName}.`));
70408
70589
  }
70409
70590
  } catch (err) {
70410
70591
  console.error(source_default.red(`Error stopping daemon: ${err.message}`));
@@ -70583,6 +70764,172 @@ import { exec } from "child_process";
70583
70764
  import fs7 from "fs/promises";
70584
70765
  import path8 from "path";
70585
70766
  import os5 from "os";
70767
+ function getHsyncDir2() {
70768
+ return path8.join(os5.homedir(), ".hsync");
70769
+ }
70770
+ function isProcessAlive2(pid) {
70771
+ try {
70772
+ process.kill(pid, 0);
70773
+ return true;
70774
+ } catch {
70775
+ return false;
70776
+ }
70777
+ }
70778
+ function jobNameFromPidFile2(filename) {
70779
+ if (filename === "daemon.pid") {
70780
+ return "all jobs";
70781
+ }
70782
+ return filename.replace("daemon-", "").replace(".pid", "");
70783
+ }
70784
+ async function readPidRecord2(pidFile) {
70785
+ try {
70786
+ const pidText = await fs7.readFile(pidFile, "utf8");
70787
+ const pid = parseInt(pidText.trim(), 10);
70788
+ if (isNaN(pid)) {
70789
+ await fs7.unlink(pidFile);
70790
+ return null;
70791
+ }
70792
+ if (!isProcessAlive2(pid)) {
70793
+ await fs7.unlink(pidFile);
70794
+ return null;
70795
+ }
70796
+ return { pid };
70797
+ } catch {
70798
+ return null;
70799
+ }
70800
+ }
70801
+ async function listAllDaemonPids2() {
70802
+ const hsyncDir = getHsyncDir2();
70803
+ const results = [];
70804
+ try {
70805
+ const files = await fs7.readdir(hsyncDir);
70806
+ for (const file of files) {
70807
+ if (!file.startsWith("daemon") || !file.endsWith(".pid")) {
70808
+ continue;
70809
+ }
70810
+ const pidFile = path8.join(hsyncDir, file);
70811
+ const record = await readPidRecord2(pidFile);
70812
+ if (record) {
70813
+ results.push({
70814
+ pidFile,
70815
+ pid: record.pid,
70816
+ jobName: jobNameFromPidFile2(file)
70817
+ });
70818
+ }
70819
+ }
70820
+ } catch {}
70821
+ return results;
70822
+ }
70823
+ var RECONCILE_INTERVAL_MS2 = 1e4;
70824
+ async function executeSyncPass2(jobId, activeJobs) {
70825
+ if (activeJobs.has(jobId)) {
70826
+ const config = await loadConfig();
70827
+ const busyJob = config.syncJobs?.find((j) => j.id === jobId);
70828
+ const busyName = busyJob?.name || jobId;
70829
+ console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${busyName}] Skip run — previous run still in progress.`));
70830
+ return;
70831
+ }
70832
+ const freshConfig = await loadConfig();
70833
+ const freshServerUrl = freshConfig.serverUrl;
70834
+ const freshApiKey = freshConfig.apiKey;
70835
+ const freshJob = freshConfig.syncJobs?.find((j) => j.id === jobId);
70836
+ if (!freshJob) {
70837
+ return;
70838
+ }
70839
+ if (!freshServerUrl || !freshApiKey) {
70840
+ console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Missing server URL or API key. Run: hsync login`));
70841
+ return;
70842
+ }
70843
+ if (!freshJob.enabled) {
70844
+ return;
70845
+ }
70846
+ activeJobs.add(jobId);
70847
+ const stateDbPath = path8.join(getHsyncDir2(), "state", `${freshJob.id}.db`);
70848
+ const engine = new SyncEngine(freshJob, freshServerUrl, freshApiKey, stateDbPath);
70849
+ let uploadCount = 0;
70850
+ let downloadCount = 0;
70851
+ let skipCount = 0;
70852
+ let errorCount = 0;
70853
+ engine.on((event) => {
70854
+ const time = new Date().toLocaleTimeString();
70855
+ if (event.type === "start") {
70856
+ console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Running scan...`));
70857
+ } else if (event.type === "scan-complete") {
70858
+ console.log(source_default.blue(`[${time}] [Sync: ${freshJob.name}] Scan completed.`));
70859
+ } else if (event.type === "upload-complete") {
70860
+ uploadCount++;
70861
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Uploaded: ${event.path}`));
70862
+ } else if (event.type === "download-complete") {
70863
+ downloadCount++;
70864
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Downloaded: ${event.path}`));
70865
+ } else if (event.type === "skip") {
70866
+ skipCount++;
70867
+ } else if (event.type === "conflict") {
70868
+ console.log(source_default.yellow(`[${time}] [Sync: ${freshJob.name}] ⚠ Conflict resolved for: ${event.path}`));
70869
+ } else if (event.type === "error") {
70870
+ errorCount++;
70871
+ console.error(source_default.red(`[${time}] [Sync: ${freshJob.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
70872
+ } else if (event.type === "complete") {
70873
+ const summary = `${uploadCount} uploaded, ${downloadCount} downloaded, ${skipCount} unchanged`;
70874
+ if (uploadCount === 0 && downloadCount === 0 && errorCount === 0) {
70875
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed (${summary}).`));
70876
+ } else {
70877
+ console.log(source_default.green(`[${time}] [Sync: ${freshJob.name}] ✔ Sync pass completed successfully (${summary}).`));
70878
+ }
70879
+ }
70880
+ });
70881
+ try {
70882
+ await engine.run();
70883
+ } catch (runErr) {
70884
+ console.error(source_default.red(`[${new Date().toLocaleTimeString()}] [Sync: ${freshJob.name}] Sync crash: ${runErr.message}`));
70885
+ } finally {
70886
+ activeJobs.delete(jobId);
70887
+ }
70888
+ }
70889
+ function scheduleSyncJob2(job, activeJobs, scheduledJobs) {
70890
+ const intervalDelay = job.intervalMs || 1e4;
70891
+ const triggerSyncRun = async () => {
70892
+ await executeSyncPass2(job.id, activeJobs);
70893
+ };
70894
+ triggerSyncRun();
70895
+ const intervalId = setInterval(triggerSyncRun, intervalDelay);
70896
+ scheduledJobs.set(job.id, { intervalId, intervalMs: intervalDelay });
70897
+ console.log(source_default.green(`[${new Date().toLocaleTimeString()}] [Scheduler] Registered: ${job.name} (every ${intervalDelay / 1000}s)`));
70898
+ }
70899
+ function unscheduleSyncJob2(jobId, jobName, scheduledJobs) {
70900
+ const entry = scheduledJobs.get(jobId);
70901
+ if (!entry) {
70902
+ return;
70903
+ }
70904
+ clearInterval(entry.intervalId);
70905
+ scheduledJobs.delete(jobId);
70906
+ console.log(source_default.yellow(`[${new Date().toLocaleTimeString()}] [Scheduler] Unregistered: ${jobName || jobId}`));
70907
+ }
70908
+ async function reconcileSchedules2(activeJobs, scheduledJobs, singleJobName) {
70909
+ const config = await loadConfig();
70910
+ let jobs = (config.syncJobs || []).filter((j) => j.enabled);
70911
+ if (singleJobName) {
70912
+ jobs = jobs.filter((j) => j.name === singleJobName);
70913
+ }
70914
+ const enabledIds = new Set(jobs.map((j) => j.id));
70915
+ for (const [jobId, entry] of scheduledJobs) {
70916
+ if (!enabledIds.has(jobId)) {
70917
+ const staleJob = config.syncJobs?.find((j) => j.id === jobId);
70918
+ unscheduleSyncJob2(jobId, staleJob?.name, scheduledJobs);
70919
+ continue;
70920
+ }
70921
+ const job = jobs.find((j) => j.id === jobId);
70922
+ if (job && entry.intervalMs !== (job.intervalMs || 1e4)) {
70923
+ unscheduleSyncJob2(jobId, job.name, scheduledJobs);
70924
+ scheduleSyncJob2(job, activeJobs, scheduledJobs);
70925
+ }
70926
+ }
70927
+ for (const job of jobs) {
70928
+ if (!scheduledJobs.has(job.id)) {
70929
+ scheduleSyncJob2(job, activeJobs, scheduledJobs);
70930
+ }
70931
+ }
70932
+ }
70586
70933
  function parseRemoteTarget3(target) {
70587
70934
  const parts = target.split(":");
70588
70935
  const slug = parts[0];
@@ -70591,37 +70938,44 @@ function parseRemoteTarget3(target) {
70591
70938
  }
70592
70939
  async function startSyncScheduler2(name, options) {
70593
70940
  const isDaemon = options?.daemon;
70594
- const hsyncDir = path8.join(os5.homedir(), ".hsync");
70595
- const pidFile = path8.join(hsyncDir, name ? `daemon-${name}.pid` : "daemon.pid");
70941
+ let foregroundJobName = name;
70942
+ const hsyncDir = getHsyncDir2();
70943
+ const globalPidFile = path8.join(hsyncDir, "daemon.pid");
70944
+ if (isDaemon && foregroundJobName) {
70945
+ console.log(source_default.yellow(`The sync daemon manages all enabled jobs. Starting global daemon instead of "${foregroundJobName}" only.`));
70946
+ foregroundJobName = undefined;
70947
+ }
70596
70948
  if (isDaemon) {
70597
70949
  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], {
70950
+ const existing = await readPidRecord2(globalPidFile);
70951
+ if (existing) {
70952
+ console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${existing.pid}).`));
70953
+ console.log(source_default.gray("New sync jobs are picked up automatically — no restart needed."));
70954
+ return;
70955
+ }
70956
+ const legacyDaemons = await listAllDaemonPids2();
70957
+ const perJobDaemons = legacyDaemons.filter((d) => d.jobName !== "all jobs");
70958
+ if (perJobDaemons.length > 0) {
70959
+ console.log(source_default.yellow("Warning: Legacy per-job daemons are still running. Stop them with: hsync sync stop"));
70960
+ }
70961
+ const logFile = path8.join(hsyncDir, "daemon.log");
70962
+ console.log(source_default.cyan(`⚙️ Starting global sync daemon in the background...`));
70963
+ const bunExec = process.execPath;
70964
+ const scriptPath = Bun.main;
70965
+ const spawnArgs = ["sync", "start"];
70966
+ const logFileHandle = await fs7.open(logFile, "a");
70967
+ const subprocess = Bun.spawn([bunExec, scriptPath, ...spawnArgs], {
70614
70968
  detached: true,
70615
70969
  stdout: logFileHandle.fd,
70616
70970
  stderr: logFileHandle.fd,
70617
70971
  stdin: "ignore"
70618
70972
  });
70619
- const pid = subprocess.pid;
70620
- await fs7.writeFile(pidFile, pid.toString(), "utf8");
70621
70973
  subprocess.unref();
70622
- console.log(source_default.green(`✔ Sync daemon started successfully!`));
70623
- console.log(`PID: ${pid}`);
70624
- console.log(`Logs: tail -f ${logFile}`);
70974
+ await logFileHandle.close();
70975
+ console.log(source_default.green(`✔ Global sync daemon started successfully!`));
70976
+ console.log(source_default.gray("All enabled sync jobs will be scheduled automatically."));
70977
+ console.log(`Logs: hsync sync logs -f`);
70978
+ console.log(source_default.gray("Stop with: hsync sync stop"));
70625
70979
  return;
70626
70980
  }
70627
70981
  try {
@@ -70632,11 +70986,12 @@ async function startSyncScheduler2(name, options) {
70632
70986
  console.error(source_default.red("Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login"));
70633
70987
  process.exit(1);
70634
70988
  }
70989
+ const isGlobalScheduler = !foregroundJobName;
70635
70990
  let jobs = config.syncJobs || [];
70636
- if (name) {
70637
- jobs = jobs.filter((j) => j.name === name);
70991
+ if (foregroundJobName) {
70992
+ jobs = jobs.filter((j) => j.name === foregroundJobName);
70638
70993
  if (jobs.length === 0) {
70639
- console.error(source_default.red(`Error: Sync job named "${name}" was not found or is disabled.`));
70994
+ console.error(source_default.red(`Error: Sync job named "${foregroundJobName}" was not found or is disabled.`));
70640
70995
  process.exit(1);
70641
70996
  }
70642
70997
  } else {
@@ -70647,10 +71002,10 @@ async function startSyncScheduler2(name, options) {
70647
71002
  return;
70648
71003
  }
70649
71004
  await fs7.mkdir(hsyncDir, { recursive: true });
70650
- await fs7.writeFile(pidFile, process.pid.toString(), "utf8");
71005
+ await fs7.writeFile(globalPidFile, process.pid.toString(), "utf8");
70651
71006
  const cleanPidFile = async () => {
70652
71007
  try {
70653
- await fs7.unlink(pidFile);
71008
+ await fs7.unlink(globalPidFile);
70654
71009
  } catch {}
70655
71010
  };
70656
71011
  process.on("SIGINT", async () => {
@@ -70664,47 +71019,23 @@ async function startSyncScheduler2(name, options) {
70664
71019
  console.log(source_default.cyan(`
70665
71020
  === Starting Sync Scheduler Service ===`));
70666
71021
  console.log(source_default.cyan(`Target Server: ${serverUrl}`));
70667
- console.log(source_default.cyan(`Active Profiles: ${jobs.map((j) => j.name).join(", ")}
71022
+ if (isGlobalScheduler) {
71023
+ console.log(source_default.cyan(`Mode: Global daemon (all enabled jobs, auto-discovers new jobs)`));
71024
+ console.log(source_default.cyan(`Currently registered: ${jobs.map((j) => j.name).join(", ")}
70668
71025
  `));
71026
+ } else {
71027
+ console.log(source_default.cyan(`Active Profile: ${jobs.map((j) => j.name).join(", ")}
71028
+ `));
71029
+ }
70669
71030
  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
- }
71031
+ const scheduledJobs = new Map;
71032
+ await reconcileSchedules2(activeJobs, scheduledJobs, foregroundJobName);
71033
+ if (isGlobalScheduler) {
71034
+ setInterval(() => {
71035
+ reconcileSchedules2(activeJobs, scheduledJobs).catch((reconcileErr) => {
71036
+ console.error(source_default.red(`[Scheduler] Failed to refresh job list: ${reconcileErr.message}`));
70697
71037
  });
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);
71038
+ }, RECONCILE_INTERVAL_MS2);
70708
71039
  }
70709
71040
  } catch (err) {
70710
71041
  console.error(source_default.red(`Scheduler crashed: ${err.message}`));
@@ -70713,40 +71044,33 @@ async function startSyncScheduler2(name, options) {
70713
71044
  }
70714
71045
  async function stopSyncScheduler2(name) {
70715
71046
  try {
70716
- const hsyncDir = path8.join(os5.homedir(), ".hsync");
70717
71047
  if (name) {
70718
- const pidFile = path8.join(hsyncDir, `daemon-${name}.pid`);
71048
+ console.log(source_default.yellow(`The sync daemon manages all jobs together. Stopping global daemon (includes "${name}")...`));
71049
+ }
71050
+ const globalFile = path8.join(getHsyncDir2(), "daemon.pid");
71051
+ const globalDaemon = await readPidRecord2(globalFile);
71052
+ if (globalDaemon) {
71053
+ process.kill(globalDaemon.pid, "SIGTERM");
70719
71054
  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
- }
71055
+ await fs7.unlink(globalFile);
71056
+ } catch {}
71057
+ console.log(source_default.green(`✔ Global sync daemon stopped (PID: ${globalDaemon.pid}).`));
70728
71058
  } 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
- }
71059
+ console.log(source_default.yellow("Global sync daemon is not running."));
71060
+ console.log(source_default.gray("Check with: hsync sync status"));
71061
+ }
71062
+ const legacyDaemons = await listAllDaemonPids2();
71063
+ for (const daemon of legacyDaemons) {
71064
+ if (daemon.jobName === "all jobs") {
71065
+ continue;
70749
71066
  }
71067
+ try {
71068
+ process.kill(daemon.pid, "SIGTERM");
71069
+ } catch {}
71070
+ try {
71071
+ await fs7.unlink(daemon.pidFile);
71072
+ } catch {}
71073
+ console.log(source_default.green(`✔ Stopped legacy per-job daemon (PID: ${daemon.pid}) for ${daemon.jobName}.`));
70750
71074
  }
70751
71075
  } catch (err) {
70752
71076
  console.error(source_default.red(`Error stopping daemon: ${err.message}`));
@@ -70914,29 +71238,22 @@ async function startUiServer(options) {
70914
71238
  });
70915
71239
  localRouter.get("/jobs/:name/status", async (req, res) => {
70916
71240
  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");
71241
+ const globalPidFile = path9.join(os6.homedir(), ".hsync", "daemon.pid");
70921
71242
  let pidText = "";
70922
71243
  try {
70923
- pidText = await fs8.readFile(pidFile, "utf8");
71244
+ pidText = await fs8.readFile(globalPidFile, "utf8");
70924
71245
  } catch {
70925
- try {
70926
- pidText = await fs8.readFile(globalPidFile, "utf8");
70927
- } catch {
70928
- return res.json({ running: false });
70929
- }
71246
+ return res.json({ running: false, mode: "global" });
70930
71247
  }
70931
71248
  const pid = parseInt(pidText.trim(), 10);
70932
71249
  if (isNaN(pid)) {
70933
- return res.json({ running: false });
71250
+ return res.json({ running: false, mode: "global" });
70934
71251
  }
70935
71252
  try {
70936
71253
  process.kill(pid, 0);
70937
- res.json({ running: true, pid });
71254
+ res.json({ running: true, pid, mode: "global" });
70938
71255
  } catch {
70939
- res.json({ running: false });
71256
+ res.json({ running: false, mode: "global" });
70940
71257
  }
70941
71258
  } catch (err) {
70942
71259
  res.status(500).json({ error: err.message });
@@ -70944,18 +71261,16 @@ async function startUiServer(options) {
70944
71261
  });
70945
71262
  localRouter.post("/jobs/:name/start", async (req, res) => {
70946
71263
  try {
70947
- const jobName = req.params.name;
70948
- await startSyncScheduler2(jobName, { daemon: true });
70949
- res.json({ success: true });
71264
+ await startSyncScheduler2(undefined, { daemon: true });
71265
+ res.json({ success: true, mode: "global" });
70950
71266
  } catch (err) {
70951
71267
  res.status(500).json({ error: err.message });
70952
71268
  }
70953
71269
  });
70954
71270
  localRouter.post("/jobs/:name/stop", async (req, res) => {
70955
71271
  try {
70956
- const jobName = req.params.name;
70957
- await stopSyncScheduler2(jobName);
70958
- res.json({ success: true });
71272
+ await stopSyncScheduler2();
71273
+ res.json({ success: true, mode: "global" });
70959
71274
  } catch (err) {
70960
71275
  res.status(500).json({ error: err.message });
70961
71276
  }
@@ -71100,7 +71415,7 @@ Press Ctrl+C to stop the server...
71100
71415
  // package.json
71101
71416
  var package_default = {
71102
71417
  name: "hsync-cli",
71103
- version: "1.1.1",
71418
+ version: "1.1.2",
71104
71419
  description: "hsync CLI for file sync operations",
71105
71420
  bin: {
71106
71421
  hsync: "./dist/bin.js"
@@ -71177,12 +71492,15 @@ syncCmd.command("list").description("List all registered local sync profiles").a
71177
71492
  syncCmd.command("remove <name>").description("Remove a registered local sync job").action(async (name) => {
71178
71493
  await removeSyncJob(name);
71179
71494
  });
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) => {
71495
+ 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
71496
  await startSyncScheduler(name, options);
71182
71497
  });
71183
- syncCmd.command("stop [name]").description("Stop active background synchronization scheduler daemon").action(async (name) => {
71498
+ syncCmd.command("stop [name]").description("Stop the global background sync daemon").action(async (name) => {
71184
71499
  await stopSyncScheduler(name);
71185
71500
  });
71501
+ syncCmd.command("status").description("Show active background sync daemons").action(async () => {
71502
+ await showSyncStatus();
71503
+ });
71186
71504
  syncCmd.command("autostart <action>").description('Configure hsync sync autostart daemon on login (action: "enable" | "disable")').action(async (action) => {
71187
71505
  if (action === "enable") {
71188
71506
  await configureAutostart(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hsync-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "hsync CLI for file sync operations",
5
5
  "bin": {
6
6
  "hsync": "./dist/bin.js"