hsync-cli 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,6 +1,5 @@
1
- #!/usr/bin/env node
2
-
3
1
  #!/usr/bin/env bun
2
+
4
3
  // @bun
5
4
  import { createRequire } from "node:module";
6
5
  var __create = Object.create;
@@ -45776,6 +45775,7 @@ import path2 from "path";
45776
45775
  import os3 from "os";
45777
45776
  var configDir = path2.join(os3.homedir(), ".hsync");
45778
45777
  var configPath = path2.join(configDir, "config.json");
45778
+ var DEFAULT_SERVER_URL = "https://hsync-backend.giftsh.in";
45779
45779
  async function loadConfig() {
45780
45780
  try {
45781
45781
  const raw = await fs.readFile(configPath, "utf-8");
@@ -45783,9 +45783,15 @@ async function loadConfig() {
45783
45783
  if (!parsed.syncJobs) {
45784
45784
  parsed.syncJobs = [];
45785
45785
  }
45786
+ if (!parsed.serverUrl) {
45787
+ parsed.serverUrl = DEFAULT_SERVER_URL;
45788
+ }
45786
45789
  return parsed;
45787
45790
  } catch (err) {
45788
- return { syncJobs: [] };
45791
+ return {
45792
+ serverUrl: DEFAULT_SERVER_URL,
45793
+ syncJobs: []
45794
+ };
45789
45795
  }
45790
45796
  }
45791
45797
  async function saveConfig(config) {
@@ -51728,7 +51734,41 @@ async function removeSyncJob(name) {
51728
51734
  process.exit(1);
51729
51735
  }
51730
51736
  }
51731
- async function startSyncScheduler(name) {
51737
+ async function startSyncScheduler(name, options) {
51738
+ const isDaemon = options?.daemon;
51739
+ const hsyncDir = path7.join(os4.homedir(), ".hsync");
51740
+ const pidFile = path7.join(hsyncDir, name ? `daemon-${name}.pid` : "daemon.pid");
51741
+ if (isDaemon) {
51742
+ await fs6.mkdir(hsyncDir, { recursive: true });
51743
+ try {
51744
+ const existingPidText = await fs6.readFile(pidFile, "utf8");
51745
+ if (existingPidText) {
51746
+ const pid2 = parseInt(existingPidText.trim(), 10);
51747
+ try {
51748
+ process.kill(pid2, 0);
51749
+ console.log(source_default.yellow(`Warning: Sync daemon is already running (PID: ${pid2}).`));
51750
+ return;
51751
+ } catch {}
51752
+ }
51753
+ } catch {}
51754
+ const logFile = path7.join(hsyncDir, name ? `daemon-${name}.log` : "daemon.log");
51755
+ console.log(source_default.cyan(`⚙️ Starting sync daemon in the background...`));
51756
+ const filteredArgs = process.argv.slice(1).filter((arg) => arg !== "--daemon" && arg !== "-d");
51757
+ const logFileHandle = await fs6.open(logFile, "w");
51758
+ const subprocess = Bun.spawn([process.argv[0], ...filteredArgs], {
51759
+ detached: true,
51760
+ stdout: logFileHandle.fd,
51761
+ stderr: logFileHandle.fd,
51762
+ stdin: "ignore"
51763
+ });
51764
+ const pid = subprocess.pid;
51765
+ await fs6.writeFile(pidFile, pid.toString(), "utf8");
51766
+ subprocess.unref();
51767
+ console.log(source_default.green(`✔ Sync daemon started successfully!`));
51768
+ console.log(`PID: ${pid}`);
51769
+ console.log(`Logs: tail -f ${logFile}`);
51770
+ return;
51771
+ }
51732
51772
  try {
51733
51773
  const config = await loadConfig();
51734
51774
  const serverUrl = config.serverUrl;
@@ -51751,6 +51791,21 @@ async function startSyncScheduler(name) {
51751
51791
  console.error(source_default.yellow("Warning: No active sync jobs to schedule. Register a sync job first."));
51752
51792
  return;
51753
51793
  }
51794
+ await fs6.mkdir(hsyncDir, { recursive: true });
51795
+ await fs6.writeFile(pidFile, process.pid.toString(), "utf8");
51796
+ const cleanPidFile = async () => {
51797
+ try {
51798
+ await fs6.unlink(pidFile);
51799
+ } catch {}
51800
+ };
51801
+ process.on("SIGINT", async () => {
51802
+ await cleanPidFile();
51803
+ process.exit(0);
51804
+ });
51805
+ process.on("SIGTERM", async () => {
51806
+ await cleanPidFile();
51807
+ process.exit(0);
51808
+ });
51754
51809
  console.log(source_default.cyan(`
51755
51810
  === Starting Sync Scheduler Service ===`));
51756
51811
  console.log(source_default.cyan(`Target Server: ${serverUrl}`));
@@ -51801,6 +51856,208 @@ async function startSyncScheduler(name) {
51801
51856
  process.exit(1);
51802
51857
  }
51803
51858
  }
51859
+ async function stopSyncScheduler(name) {
51860
+ try {
51861
+ const hsyncDir = path7.join(os4.homedir(), ".hsync");
51862
+ if (name) {
51863
+ const pidFile = path7.join(hsyncDir, `daemon-${name}.pid`);
51864
+ try {
51865
+ const pidText = await fs6.readFile(pidFile, "utf8");
51866
+ const pid = parseInt(pidText.trim(), 10);
51867
+ process.kill(pid, "SIGTERM");
51868
+ await fs6.unlink(pidFile);
51869
+ console.log(source_default.green(`✔ Sync daemon for "${name}" (PID: ${pid}) has been stopped.`));
51870
+ } catch {
51871
+ console.error(source_default.red(`Error: No active sync daemon found for "${name}".`));
51872
+ }
51873
+ } else {
51874
+ const files = await fs6.readdir(hsyncDir);
51875
+ const pidFiles = files.filter((f) => f.startsWith("daemon") && f.endsWith(".pid"));
51876
+ if (pidFiles.length === 0) {
51877
+ console.log(source_default.yellow("No active sync daemons found running."));
51878
+ return;
51879
+ }
51880
+ for (const file of pidFiles) {
51881
+ const pidFile = path7.join(hsyncDir, file);
51882
+ try {
51883
+ const pidText = await fs6.readFile(pidFile, "utf8");
51884
+ const pid = parseInt(pidText.trim(), 10);
51885
+ process.kill(pid, "SIGTERM");
51886
+ await fs6.unlink(pidFile);
51887
+ const jobName = file.replace("daemon-", "").replace(".pid", "");
51888
+ console.log(source_default.green(`✔ Stopped sync daemon (PID: ${pid}) for ${jobName === "daemon" ? "all jobs" : jobName}.`));
51889
+ } catch {
51890
+ try {
51891
+ await fs6.unlink(pidFile);
51892
+ } catch {}
51893
+ }
51894
+ }
51895
+ }
51896
+ } catch (err) {
51897
+ console.error(source_default.red(`Error stopping daemon: ${err.message}`));
51898
+ }
51899
+ }
51900
+ async function configureAutostart(enable) {
51901
+ try {
51902
+ const platform = os4.platform();
51903
+ if (platform === "linux") {
51904
+ const autostartDir = path7.join(os4.homedir(), ".config", "autostart");
51905
+ const desktopFile = path7.join(autostartDir, "hsync-autostart.desktop");
51906
+ if (enable) {
51907
+ await fs6.mkdir(autostartDir, { recursive: true });
51908
+ const content = `[Desktop Entry]
51909
+ Type=Application
51910
+ Exec=hsync sync start --daemon
51911
+ Hidden=false
51912
+ NoDisplay=false
51913
+ X-GNOME-Autostart-enabled=true
51914
+ Name=hsync Autostart Daemon
51915
+ Comment=Start hsync background synchronization daemon on login
51916
+ `;
51917
+ await fs6.writeFile(desktopFile, content, "utf8");
51918
+ await fs6.chmod(desktopFile, 493);
51919
+ console.log(source_default.green("✔ Autostart successfully enabled!"));
51920
+ console.log(`Created autostart entry: ${desktopFile}`);
51921
+ } else {
51922
+ try {
51923
+ await fs6.unlink(desktopFile);
51924
+ console.log(source_default.green("✔ Autostart successfully disabled."));
51925
+ } catch {
51926
+ console.log(source_default.yellow("Autostart was not enabled."));
51927
+ }
51928
+ }
51929
+ } else if (platform === "win32") {
51930
+ const appData = process.env.APPDATA;
51931
+ if (!appData) {
51932
+ throw new Error("APPDATA environment variable not found.");
51933
+ }
51934
+ const startupDir = path7.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
51935
+ const batFile = path7.join(startupDir, "hsync-autostart.bat");
51936
+ if (enable) {
51937
+ await fs6.mkdir(startupDir, { recursive: true });
51938
+ const content = `@echo off
51939
+ hsync sync start --daemon
51940
+ `;
51941
+ await fs6.writeFile(batFile, content, "utf8");
51942
+ console.log(source_default.green("✔ Windows Autostart successfully enabled!"));
51943
+ console.log(`Created startup script: ${batFile}`);
51944
+ } else {
51945
+ try {
51946
+ await fs6.unlink(batFile);
51947
+ console.log(source_default.green("✔ Windows Autostart successfully disabled."));
51948
+ } catch {
51949
+ console.log(source_default.yellow("Windows Autostart was not enabled."));
51950
+ }
51951
+ }
51952
+ } else {
51953
+ console.error(source_default.red(`Error: Autostart configuration is not natively supported on platform "${platform}".`));
51954
+ }
51955
+ } catch (err) {
51956
+ console.error(source_default.red(`Error configuring autostart: ${err.message}`));
51957
+ }
51958
+ }
51959
+ async function showSyncLogs(name, options) {
51960
+ const hsyncDir = path7.join(os4.homedir(), ".hsync");
51961
+ const logFile = path7.join(hsyncDir, name ? `daemon-${name}.log` : "daemon.log");
51962
+ try {
51963
+ await fs6.access(logFile);
51964
+ } catch {
51965
+ console.log(source_default.yellow("No sync logs found. Either the daemon has not run yet, or logs were cleaned."));
51966
+ return;
51967
+ }
51968
+ if (options?.follow) {
51969
+ console.log(source_default.cyan(`=== Streaming Sync Logs (Ctrl+C to exit) ===`));
51970
+ let fileHandle = await fs6.open(logFile, "r");
51971
+ let stat = await fileHandle.stat();
51972
+ let currentSize = stat.size;
51973
+ const startPos = Math.max(0, currentSize - 5000);
51974
+ const buffer = Buffer.alloc(currentSize - startPos);
51975
+ if (buffer.length > 0) {
51976
+ await fileHandle.read(buffer, 0, buffer.length, startPos);
51977
+ process.stdout.write(buffer.toString("utf8"));
51978
+ }
51979
+ const watcher = fs6.watch(logFile);
51980
+ try {
51981
+ for await (const event of watcher) {
51982
+ if (event.eventType === "change") {
51983
+ const newStat = await fs6.stat(logFile);
51984
+ const newSize = newStat.size;
51985
+ if (newSize > currentSize) {
51986
+ const sizeDiff = newSize - currentSize;
51987
+ const readBuffer = Buffer.alloc(sizeDiff);
51988
+ const tempHandle = await fs6.open(logFile, "r");
51989
+ await tempHandle.read(readBuffer, 0, sizeDiff, currentSize);
51990
+ await tempHandle.close();
51991
+ process.stdout.write(readBuffer.toString("utf8"));
51992
+ currentSize = newSize;
51993
+ }
51994
+ }
51995
+ }
51996
+ } catch (watchErr) {} finally {
51997
+ await fileHandle.close();
51998
+ }
51999
+ } else {
52000
+ const content = await fs6.readFile(logFile, "utf8");
52001
+ const lines = content.split(`
52002
+ `);
52003
+ const lastLines = lines.slice(-200).join(`
52004
+ `);
52005
+ process.stdout.write(lastLines);
52006
+ }
52007
+ }
52008
+ async function editSyncJob(name, options) {
52009
+ try {
52010
+ const config = await loadConfig();
52011
+ const jobs = config.syncJobs || [];
52012
+ const job = jobs.find((j) => j.name === name);
52013
+ if (!job) {
52014
+ throw new Error(`Sync job named "${name}" not found.`);
52015
+ }
52016
+ if (options.localPath) {
52017
+ const resolvedLocalPath = path7.resolve(options.localPath);
52018
+ try {
52019
+ await fs6.access(resolvedLocalPath);
52020
+ } catch {
52021
+ console.error(source_default.red(`Error: Local path "${options.localPath}" does not exist.`));
52022
+ process.exit(1);
52023
+ }
52024
+ job.localPath = resolvedLocalPath;
52025
+ }
52026
+ if (options.remoteTarget) {
52027
+ const remote = parseRemoteTarget2(options.remoteTarget);
52028
+ job.remoteBucket = remote.slug;
52029
+ job.remotePath = remote.remotePath;
52030
+ }
52031
+ if (options.direction) {
52032
+ job.direction = options.direction;
52033
+ }
52034
+ if (options.strategy) {
52035
+ job.conflictStrategy = options.strategy;
52036
+ }
52037
+ if (options.interval) {
52038
+ const intervalSec = parseInt(options.interval, 10);
52039
+ if (isNaN(intervalSec) || intervalSec <= 0) {
52040
+ throw new Error("Interval must be a positive integer in seconds.");
52041
+ }
52042
+ job.intervalMs = intervalSec * 1000;
52043
+ }
52044
+ if (options.noConflictCopy !== undefined) {
52045
+ job.conflictCopy = !options.noConflictCopy;
52046
+ } else if (options.conflictCopy !== undefined) {
52047
+ job.conflictCopy = options.conflictCopy;
52048
+ }
52049
+ await saveConfig(config);
52050
+ console.log(source_default.green(`✔ Sync job "${name}" successfully updated!`));
52051
+ console.log(`Local Path: ${job.localPath}`);
52052
+ console.log(`Remote: ${job.remoteBucket}:${job.remotePath || ""}`);
52053
+ console.log(`Direction: ${job.direction}`);
52054
+ console.log(`Strategy: ${job.conflictStrategy}`);
52055
+ console.log(`Interval: ${(job.intervalMs || 1e4) / 1000} seconds`);
52056
+ } catch (err) {
52057
+ console.error(source_default.red(`Error: Failed to update sync job: ${err.message}`));
52058
+ process.exit(1);
52059
+ }
52060
+ }
51804
52061
 
51805
52062
  // src/index.ts
51806
52063
  var program2 = new Command;
@@ -51844,7 +52101,26 @@ syncCmd.command("list").description("List all registered local sync profiles").a
51844
52101
  syncCmd.command("remove <name>").description("Remove a registered local sync job").action(async (name) => {
51845
52102
  await removeSyncJob(name);
51846
52103
  });
51847
- syncCmd.command("start [name]").description("Start interval synchronization scheduler service").action(async (name) => {
51848
- await startSyncScheduler(name);
52104
+ 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) => {
52105
+ await startSyncScheduler(name, options);
52106
+ });
52107
+ syncCmd.command("stop [name]").description("Stop active background synchronization scheduler daemon").action(async (name) => {
52108
+ await stopSyncScheduler(name);
52109
+ });
52110
+ syncCmd.command("autostart <action>").description('Configure hsync sync autostart daemon on login (action: "enable" | "disable")').action(async (action) => {
52111
+ if (action === "enable") {
52112
+ await configureAutostart(true);
52113
+ } else if (action === "disable") {
52114
+ await configureAutostart(false);
52115
+ } else {
52116
+ console.error('Error: Invalid action. Use "enable" or "disable".');
52117
+ process.exit(1);
52118
+ }
52119
+ });
52120
+ syncCmd.command("logs [name]").description("View active sync scheduler daemon output logs").option("-f, --follow", "Stream live logs in real-time (continuous follow mode)").action(async (name, options) => {
52121
+ await showSyncLogs(name, options);
52122
+ });
52123
+ syncCmd.command("edit <name>").description("Edit parameters of an existing persistent sync job profile").option("-l, --localPath <path>", "Local folder path to synchronize").option("-r, --remoteTarget <target>", "Remote target bucket and subpath (slug:[path])").option("-d, --direction <push|pull|bidirectional>", "Sync transfer direction mapping").option("-s, --strategy <local-wins|server-wins|newer-wins>", "Conflict resolution strategy rules").option("-i, --interval <seconds>", "Synchronization scheduling delay frequency in seconds").option("--no-conflict-copy", "Disable keeping backup copies (.conflict-YYYY-MM-DD) on conflict resolutions").option("--conflict-copy", "Enable keeping backup copies (.conflict-YYYY-MM-DD) on conflict resolutions").action(async (name, options) => {
52124
+ await editSyncJob(name, options);
51849
52125
  });
51850
52126
  program2.parse(process.argv);
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "hsync-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "hsync CLI for file sync operations",
5
5
  "bin": {
6
6
  "hsync": "./dist/bin.js"
7
7
  },
8
+ "files": [
9
+ "dist/bin.js"
10
+ ],
8
11
  "main": "./src/index.ts",
9
12
  "scripts": {
10
13
  "dev": "bun --watch src/index.ts",
11
- "build": "bun build src/index.ts --compile --outfile dist/hsync",
14
+ "build": "bun build src/index.ts --compile --define \"process.env.DEFAULT_SERVER_URL='https://hsync-backend.giftsh.in'\" --outfile dist/hsync",
12
15
  "build:js": "bun scripts/build-js.ts",
13
16
  "prepublishOnly": "bun run build:js",
14
17
  "start": "bun src/index.ts",
package/dist/hsync DELETED
Binary file
@@ -1,32 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
-
4
- async function buildJs() {
5
- const entry = path.resolve(__dirname, '../src/index.ts');
6
- const outfile = path.resolve(__dirname, '../dist/bin.js');
7
-
8
- console.log('Bundling CLI into a pure JS bundle for Node...');
9
-
10
- // 1. Build the JavaScript bundle with Bun targeting Node
11
- const result = await Bun.build({
12
- entrypoints: [entry],
13
- target: 'node',
14
- minify: false, // Keep it readable/debuggable for NPM users
15
- });
16
-
17
- if (!result.success) {
18
- console.error('Bundle failed:', result.logs);
19
- process.exit(1);
20
- }
21
-
22
- const rawCode = await result.outputs[0].text();
23
-
24
- // 2. Prepend Node shebang and write with executable permissions (0755)
25
- const shebang = '#!/usr/bin/env node\n\n';
26
- await fs.mkdir(path.dirname(outfile), { recursive: true });
27
- await fs.writeFile(outfile, shebang + rawCode, { mode: 0o755 });
28
-
29
- console.log(`✓ Successfully created executable JS bundle: ${outfile}`);
30
- }
31
-
32
- buildJs();
@@ -1,139 +0,0 @@
1
- import chalk from 'chalk';
2
- import inquirer from 'inquirer';
3
- import { loadConfig, saveConfig } from '../utils/config';
4
- import { getHttpClient } from '../utils/client';
5
- import axios from 'axios';
6
-
7
- export async function setServer(url: string): Promise<void> {
8
- try {
9
- const config = await loadConfig();
10
- config.serverUrl = url;
11
- await saveConfig(config);
12
- console.log(chalk.green(`✔ Server URL successfully set to: ${url}`));
13
- } catch (err: any) {
14
- console.error(chalk.red(`Error saving server URL: ${err.message}`));
15
- }
16
- }
17
-
18
- export async function login(options: { apiKey?: string }): Promise<void> {
19
- const apiKeyFlag = options.apiKey;
20
- if (apiKeyFlag) {
21
- // Headless API Key Verification
22
- try {
23
- const config = await loadConfig();
24
- config.apiKey = apiKeyFlag;
25
- await saveConfig(config);
26
-
27
- // Verify the key immediately
28
- const client = await getHttpClient();
29
- const res = await client.get('/api/auth/me');
30
- const email = res.data.email;
31
-
32
- console.log(chalk.green(`✔ Successfully authenticated headlessly!`));
33
- console.log(chalk.cyan(`Account Email: ${email}`));
34
- } catch (err: any) {
35
- // Rollback configuration on failure
36
- const config = await loadConfig();
37
- config.apiKey = undefined;
38
- await saveConfig(config);
39
- console.error(chalk.red(`Error: API Key verification failed. Make sure server is reachable and API Key is valid. (${err.message})`));
40
- process.exit(1);
41
- }
42
- return;
43
- }
44
-
45
- // Interactive Email / Password Flow
46
- try {
47
- const config = await loadConfig();
48
- const serverUrl = config.serverUrl;
49
- if (!serverUrl) {
50
- console.error(chalk.red('Error: Server URL is not configured. Please run: hsync config set-server <url>'));
51
- process.exit(1);
52
- }
53
-
54
- const answers = await inquirer.prompt([
55
- {
56
- type: 'input',
57
- name: 'email',
58
- message: 'Enter your email address:',
59
- validate: (input) => (input.trim() !== '' ? true : 'Email cannot be empty.'),
60
- },
61
- {
62
- type: 'password',
63
- name: 'password',
64
- message: 'Enter your password:',
65
- mask: '*',
66
- validate: (input) => (input.trim() !== '' ? true : 'Password cannot be empty.'),
67
- },
68
- ]);
69
-
70
- const email = answers.email;
71
- const password = answers.password;
72
-
73
- console.log(chalk.cyan('Authenticating with server...'));
74
- const loginRes = await axios.post(
75
- `${serverUrl}/api/auth/login`,
76
- { email: email, password: password },
77
- { validateStatus: (status) => status === 200 }
78
- );
79
-
80
- const cookies = loginRes.headers['set-cookie'];
81
- if (!cookies || cookies.length === 0) {
82
- throw new Error('No session cookie returned by the server.');
83
- }
84
- const sessionCookie = cookies[0];
85
-
86
- console.log(chalk.cyan('Generating secure persistent API key...'));
87
- const keyRes = await axios.post(
88
- `${serverUrl}/api/auth/keys`,
89
- { name: 'hsync CLI Key' },
90
- {
91
- headers: { Cookie: sessionCookie },
92
- validateStatus: (status) => status === 200,
93
- }
94
- );
95
-
96
- const generatedKey = keyRes.data.apiKey;
97
- config.apiKey = generatedKey;
98
- await saveConfig(config);
99
-
100
- console.log(chalk.green('✔ Successfully logged in! Secure API key has been registered locally.'));
101
- } catch (err: any) {
102
- let message = err.message;
103
- if (err.response && err.response.data && err.response.data.error) {
104
- message = err.response.data.error;
105
- }
106
- console.error(chalk.red(`Error: Authentication failed: ${message}`));
107
- process.exit(1);
108
- }
109
- }
110
-
111
- export async function showMe(): Promise<void> {
112
- try {
113
- const client = await getHttpClient();
114
- const res = await client.get('/api/auth/me');
115
- const profile = res.data;
116
-
117
- console.log(chalk.cyan('=== Active CLI Auth Session ==='));
118
- console.log(`Email: ${profile.email}`);
119
- console.log(`Role: ${profile.role}`);
120
- } catch (err: any) {
121
- let message = err.message;
122
- if (err.response && err.response.data && err.response.data.error) {
123
- message = err.response.data.error;
124
- }
125
- console.error(chalk.red(`Error: Failed to retrieve profile: ${message}`));
126
- process.exit(1);
127
- }
128
- }
129
-
130
- export async function logout(): Promise<void> {
131
- try {
132
- const config = await loadConfig();
133
- config.apiKey = undefined;
134
- await saveConfig(config);
135
- console.log(chalk.green('✔ Successfully logged out. API key removed.'));
136
- } catch (err: any) {
137
- console.error(chalk.red(`Error during logout: ${err.message}`));
138
- }
139
- }
@@ -1,110 +0,0 @@
1
- import chalk from 'chalk';
2
- import inquirer from 'inquirer';
3
- import { getHttpClient } from '../utils/client';
4
-
5
- export async function listBuckets(): Promise<void> {
6
- try {
7
- const client = await getHttpClient();
8
- const res = await client.get('/api/buckets');
9
- const buckets = res.data;
10
-
11
- if (buckets.length === 0) {
12
- console.log(chalk.yellow('No buckets found. Create one with: hsync bucket create <name> <slug> --provider <local|s3>'));
13
- return;
14
- }
15
-
16
- console.log(chalk.cyan('\n=== Registered Storage Buckets ==='));
17
- for (const b of buckets) {
18
- console.log(`- ${chalk.bold(b.name)} (${chalk.cyan(b.slug)})`);
19
- console.log(` Provider: ${b.providerType}`);
20
- if (b.providerType === 'local') {
21
- console.log(` Path: ${b.storageConfig.rootPath}`);
22
- } else {
23
- console.log(` S3 Bucket: ${b.storageConfig.bucket}`);
24
- }
25
- console.log();
26
- }
27
- } catch (err: any) {
28
- let message = err.message;
29
- if (err.response && err.response.data && err.response.data.error) {
30
- message = err.response.data.error;
31
- }
32
- console.error(chalk.red(`Error: Failed to list buckets: ${message}`));
33
- process.exit(1);
34
- }
35
- }
36
-
37
- export async function createBucket(
38
- name: string,
39
- slug: string,
40
- options: { provider: 'local' | 's3'; rootPath?: string; bucket?: string }
41
- ): Promise<void> {
42
- try {
43
- const provider = options.provider;
44
- const rootPath = options.rootPath;
45
- const bucket = options.bucket;
46
-
47
- const storageConfig: Record<string, any> = {};
48
- if (provider === 'local') {
49
- if (!rootPath) {
50
- throw new Error('--rootPath is required when using local provider.');
51
- }
52
- storageConfig.rootPath = rootPath;
53
- } else if (provider === 's3') {
54
- if (!bucket) {
55
- throw new Error('--bucket is required when using s3 provider.');
56
- }
57
- storageConfig.bucket = bucket;
58
- } else {
59
- throw new Error('Invalid provider type. Must be "local" or "s3".');
60
- }
61
-
62
- const client = await getHttpClient();
63
- const res = await client.post('/api/buckets', {
64
- name: name,
65
- slug: slug,
66
- providerType: provider,
67
- storageConfig: storageConfig,
68
- });
69
-
70
- console.log(chalk.green(`✔ Bucket successfully created!`));
71
- console.log(`Name: ${res.data.name}`);
72
- console.log(`Slug: ${res.data.slug}`);
73
- } catch (err: any) {
74
- let message = err.message;
75
- if (err.response && err.response.data && err.response.data.error) {
76
- message = err.response.data.error;
77
- }
78
- console.error(chalk.red(`Error: Failed to create bucket: ${message}`));
79
- process.exit(1);
80
- }
81
- }
82
-
83
- export async function deleteBucket(slug: string): Promise<void> {
84
- try {
85
- const answers = await inquirer.prompt([
86
- {
87
- type: 'confirm',
88
- name: 'confirm',
89
- message: `Are you absolutely sure you want to delete the bucket "${slug}" and all cached metadata? This action CANNOT be undone!`,
90
- default: false,
91
- },
92
- ]);
93
-
94
- if (!answers.confirm) {
95
- console.log(chalk.yellow('Bucket deletion cancelled.'));
96
- return;
97
- }
98
-
99
- const client = await getHttpClient();
100
- await client.delete(`/api/buckets/${slug}`);
101
- console.log(chalk.green(`✔ Bucket "${slug}" successfully deleted.`));
102
- } catch (err: any) {
103
- let message = err.message;
104
- if (err.response && err.response.data && err.response.data.error) {
105
- message = err.response.data.error;
106
- }
107
- console.error(chalk.red(`Error: Failed to delete bucket: ${message}`));
108
- process.exit(1);
109
- }
110
- }
@@ -1,213 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import os from 'os';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { loadConfig, saveConfig } from '../utils/config';
7
- import { SyncEngine } from '@file-sync/sync-engine';
8
- import type { SyncJob, SyncEvent } from '@file-sync/sync-engine';
9
-
10
- function parseRemoteTarget(target: string): { slug: string; remotePath: string } {
11
- const parts = target.split(':');
12
- const slug = parts[0];
13
- const remotePath = (parts.slice(1).join(':') || '').replace(/^\/+|\/+$/g, '');
14
- return { slug: slug, remotePath: remotePath };
15
- }
16
-
17
- export async function addSyncJob(
18
- localPath: string,
19
- remoteTarget: string,
20
- options: {
21
- name: string;
22
- direction: 'push' | 'pull' | 'bidirectional';
23
- strategy: 'local-wins' | 'server-wins' | 'newer-wins';
24
- interval?: string;
25
- noConflictCopy?: boolean;
26
- }
27
- ): Promise<void> {
28
- try {
29
- const config = await loadConfig();
30
- const resolvedLocalPath = path.resolve(localPath);
31
-
32
- // Verify local path exists
33
- try {
34
- await fs.access(resolvedLocalPath);
35
- } catch {
36
- console.error(chalk.red(`Error: Local path "${localPath}" does not exist.`));
37
- process.exit(1);
38
- }
39
-
40
- const remote = parseRemoteTarget(remoteTarget);
41
- const intervalSec = options.interval ? parseInt(options.interval, 10) : 10;
42
- if (isNaN(intervalSec) || intervalSec <= 0) {
43
- throw new Error('Interval must be a positive integer in seconds.');
44
- }
45
-
46
- // Verify unique name
47
- const existing = config.syncJobs?.find((j) => j.name === options.name);
48
- if (existing) {
49
- throw new Error(`A sync job with the name "${options.name}" already exists.`);
50
- }
51
-
52
- const newJob: SyncJob = {
53
- id: `job-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
54
- name: options.name,
55
- localPath: resolvedLocalPath,
56
- remoteBucket: remote.slug,
57
- remotePath: remote.remotePath,
58
- direction: options.direction,
59
- conflictStrategy: options.strategy,
60
- conflictCopy: !options.noConflictCopy,
61
- intervalMs: intervalSec * 1000,
62
- enabled: true,
63
- };
64
-
65
- if (!config.syncJobs) {
66
- config.syncJobs = [];
67
- }
68
- config.syncJobs.push(newJob);
69
- await saveConfig(config);
70
-
71
- console.log(chalk.green(`✔ Sync job "${options.name}" successfully registered!`));
72
- console.log(`Local Path: ${newJob.localPath}`);
73
- console.log(`Remote: ${newJob.remoteBucket}:${newJob.remotePath || ''}`);
74
- console.log(`Direction: ${newJob.direction}`);
75
- console.log(`Strategy: ${newJob.conflictStrategy}`);
76
- console.log(`Interval: ${intervalSec} seconds`);
77
- } catch (err: any) {
78
- console.error(chalk.red(`Error: Failed to register sync job: ${err.message}`));
79
- process.exit(1);
80
- }
81
- }
82
-
83
- export async function listSyncJobs(): Promise<void> {
84
- try {
85
- const config = await loadConfig();
86
- const jobs = config.syncJobs || [];
87
-
88
- if (jobs.length === 0) {
89
- console.log(chalk.yellow('No persistent sync jobs registered. Create one with: hsync sync add <localPath> <bucketSlug>:[remotePath] --name <jobName> ...'));
90
- return;
91
- }
92
-
93
- console.log(chalk.cyan('\n=== Registered Sync Profiles ==='));
94
- for (const j of jobs) {
95
- console.log(`- Name: ${chalk.bold(j.name)} (${j.enabled ? chalk.green('Enabled') : chalk.red('Disabled')})`);
96
- console.log(` Local Path: ${j.localPath}`);
97
- console.log(` Remote Path: ${j.remoteBucket}:${j.remotePath || ''}`);
98
- console.log(` Direction: ${j.direction}`);
99
- console.log(` Strategy: ${j.conflictStrategy}`);
100
- console.log(` Conflict Copy Backup: ${j.conflictCopy ? chalk.green('Yes') : chalk.red('No')}`);
101
- console.log(` Interval: ${(j.intervalMs || 10000) / 1000} seconds`);
102
- console.log();
103
- }
104
- } catch (err: any) {
105
- console.error(chalk.red(`Error listing sync jobs: ${err.message}`));
106
- }
107
- }
108
-
109
- export async function removeSyncJob(name: string): Promise<void> {
110
- try {
111
- const config = await loadConfig();
112
- const jobs = config.syncJobs || [];
113
- const index = jobs.findIndex((j) => j.name === name);
114
-
115
- if (index === -1) {
116
- throw new Error(`Sync job named "${name}" not found.`);
117
- }
118
-
119
- jobs.splice(index, 1);
120
- config.syncJobs = jobs;
121
- await saveConfig(config);
122
-
123
- console.log(chalk.green(`✔ Sync job "${name}" successfully removed.`));
124
- } catch (err: any) {
125
- console.error(chalk.red(`Error removing sync job: ${err.message}`));
126
- process.exit(1);
127
- }
128
- }
129
-
130
- export async function startSyncScheduler(name?: string): Promise<void> {
131
- try {
132
- const config = await loadConfig();
133
- const serverUrl = config.serverUrl;
134
- const apiKey = config.apiKey;
135
-
136
- if (!serverUrl || !apiKey) {
137
- console.error(chalk.red('Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login'));
138
- process.exit(1);
139
- }
140
-
141
- let jobs = config.syncJobs || [];
142
- if (name) {
143
- jobs = jobs.filter((j) => j.name === name);
144
- if (jobs.length === 0) {
145
- console.error(chalk.red(`Error: Sync job named "${name}" was not found or is disabled.`));
146
- process.exit(1);
147
- }
148
- } else {
149
- jobs = jobs.filter((j) => j.enabled);
150
- }
151
-
152
- if (jobs.length === 0) {
153
- console.error(chalk.yellow('Warning: No active sync jobs to schedule. Register a sync job first.'));
154
- return;
155
- }
156
-
157
- console.log(chalk.cyan(`\n=== Starting Sync Scheduler Service ===`));
158
- console.log(chalk.cyan(`Target Server: ${serverUrl}`));
159
- console.log(chalk.cyan(`Active Profiles: ${jobs.map((j) => j.name).join(', ')}\n`));
160
-
161
- // Overrun Lock Protection Tracker
162
- const activeJobs = new Set<string>();
163
-
164
- for (const job of jobs) {
165
- const intervalDelay = job.intervalMs || 10000;
166
- const stateDbPath = path.join(os.homedir(), '.hsync', 'state', `${job.id}.db`);
167
-
168
- const triggerSyncRun = async () => {
169
- if (activeJobs.has(job.id)) {
170
- console.log(chalk.yellow(`[${new Date().toLocaleTimeString()}] [Sync: ${job.name}] Skip run — previous run still in progress.`));
171
- return;
172
- }
173
-
174
- activeJobs.add(job.id);
175
- const engine = new SyncEngine(job, serverUrl, apiKey, stateDbPath);
176
-
177
- engine.on((event: SyncEvent) => {
178
- const time = new Date().toLocaleTimeString();
179
- if (event.type === 'start') {
180
- console.log(chalk.blue(`[${time}] [Sync: ${job.name}] Running scan...`));
181
- } else if (event.type === 'scan-complete') {
182
- console.log(chalk.blue(`[${time}] [Sync: ${job.name}] Scan completed.`));
183
- } else if (event.type === 'upload-complete') {
184
- console.log(chalk.green(`[${time}] [Sync: ${job.name}] ✔ Uploaded: ${event.path}`));
185
- } else if (event.type === 'download-complete') {
186
- console.log(chalk.green(`[${time}] [Sync: ${job.name}] ✔ Downloaded: ${event.path}`));
187
- } else if (event.type === 'conflict') {
188
- console.log(chalk.yellow(`[${time}] [Sync: ${job.name}] ⚠ Conflict resolved for: ${event.path}`));
189
- } else if (event.type === 'error') {
190
- console.error(chalk.red(`[${time}] [Sync: ${job.name}] ✘ Error on "${event.path}": ${event.error?.message}`));
191
- } else if (event.type === 'complete') {
192
- console.log(chalk.green(`[${time}] [Sync: ${job.name}] ✔ Sync pass completed successfully.`));
193
- }
194
- });
195
-
196
- try {
197
- await engine.run();
198
- } catch (runErr: any) {
199
- console.error(chalk.red(`[${new Date().toLocaleTimeString()}] [Sync: ${job.name}] Sync crash: ${runErr.message}`));
200
- } finally {
201
- activeJobs.delete(job.id);
202
- }
203
- };
204
-
205
- // Trigger immediately once, then queue interval loops
206
- triggerSyncRun();
207
- setInterval(triggerSyncRun, intervalDelay);
208
- }
209
- } catch (err: any) {
210
- console.error(chalk.red(`Scheduler crashed: ${err.message}`));
211
- process.exit(1);
212
- }
213
- }
@@ -1,220 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import crypto from 'crypto';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { loadConfig } from '../utils/config';
7
- import { OverwritePrompter } from '../utils/prompts';
8
- import { RemoteClient } from '../../../sync-engine/src/RemoteClient';
9
-
10
- interface LocalFile {
11
- relativePath: string;
12
- absolutePath: string;
13
- hash: string;
14
- size: number;
15
- }
16
-
17
- async function hashFile(absolutePath: string): Promise<string> {
18
- const content = await fs.readFile(absolutePath);
19
- return crypto.createHash('md5').update(content).digest('hex');
20
- }
21
-
22
- async function scanLocalPath(targetPath: string, rootDir: string): Promise<LocalFile[]> {
23
- const stat = await fs.stat(targetPath);
24
- if (stat.isFile()) {
25
- const hash = await hashFile(targetPath);
26
- return [{
27
- relativePath: path.basename(targetPath),
28
- absolutePath: targetPath,
29
- hash: hash,
30
- size: stat.size,
31
- }];
32
- }
33
-
34
- const results: LocalFile[] = [];
35
- async function recurse(currentPath: string) {
36
- const entries = await fs.readdir(currentPath, { withFileTypes: true });
37
- for (const entry of entries) {
38
- const fullPath = path.join(currentPath, entry.name);
39
- if (entry.isDirectory()) {
40
- await recurse(fullPath);
41
- } else if (entry.isFile()) {
42
- const fileStat = await fs.stat(fullPath);
43
- const hash = await hashFile(fullPath);
44
- results.push({
45
- relativePath: path.relative(rootDir, fullPath),
46
- absolutePath: fullPath,
47
- hash: hash,
48
- size: fileStat.size,
49
- });
50
- }
51
- }
52
- }
53
-
54
- await recurse(targetPath);
55
- return results;
56
- }
57
-
58
- function parseRemoteTarget(target: string): { slug: string; remotePath: string } {
59
- const parts = target.split(':');
60
- const slug = parts[0];
61
- const remotePath = (parts.slice(1).join(':') || '').replace(/^\/+|\/+$/g, '');
62
- return { slug: slug, remotePath: remotePath };
63
- }
64
-
65
- export async function push(localPath: string, remoteTarget: string, options: { yes?: boolean }): Promise<void> {
66
- const forceYes = options.yes || false;
67
-
68
- try {
69
- const config = await loadConfig();
70
- const serverUrl = config.serverUrl;
71
- const apiKey = config.apiKey;
72
-
73
- if (!serverUrl || !apiKey) {
74
- console.error(chalk.red('Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login'));
75
- process.exit(1);
76
- }
77
-
78
- const remote = parseRemoteTarget(remoteTarget);
79
- const resolvedLocalPath = path.resolve(localPath);
80
-
81
- // Verify local path exists
82
- try {
83
- await fs.access(resolvedLocalPath);
84
- } catch {
85
- console.error(chalk.red(`Error: Local path "${localPath}" does not exist.`));
86
- process.exit(1);
87
- }
88
-
89
- const spinner = ora('Scanning local files...').start();
90
- const localFiles = await scanLocalPath(resolvedLocalPath, resolvedLocalPath);
91
- spinner.text = 'Fetching remote file tree...';
92
-
93
- const remoteClient = new RemoteClient(serverUrl, apiKey);
94
- const remoteFiles = await remoteClient.getTree(remote.slug, remote.remotePath);
95
- spinner.stop();
96
-
97
- console.log(chalk.cyan(`Found ${localFiles.length} local files, ${remoteFiles.length} remote files.`));
98
-
99
- const prompter = new OverwritePrompter(forceYes);
100
- let pushCount = 0;
101
- let skipCount = 0;
102
-
103
- for (const localFile of localFiles) {
104
- const remoteFile = remoteFiles.find((f) => f.path === localFile.relativePath);
105
- const destinationPath = remote.remotePath
106
- ? `${remote.remotePath}/${localFile.relativePath}`
107
- : localFile.relativePath;
108
-
109
- let shouldUpload = false;
110
-
111
- if (!remoteFile) {
112
- // New file
113
- shouldUpload = true;
114
- } else if (remoteFile.hash !== localFile.hash) {
115
- // Conflict - content differs
116
- const confirm = await prompter.shouldOverwrite(localFile.relativePath);
117
- if (confirm) {
118
- shouldUpload = true;
119
- } else {
120
- skipCount++;
121
- }
122
- } else {
123
- // Identical hashes
124
- skipCount++;
125
- }
126
-
127
- if (shouldUpload) {
128
- const fileSpinner = ora(`Pushing ${localFile.relativePath}...`).start();
129
- try {
130
- await remoteClient.uploadFile(remote.slug, destinationPath, localFile.absolutePath);
131
- fileSpinner.succeed(chalk.green(`Pushed ${localFile.relativePath}`));
132
- pushCount++;
133
- } catch (uploadErr: any) {
134
- fileSpinner.fail(chalk.red(`Failed to push ${localFile.relativePath}: ${uploadErr.message}`));
135
- }
136
- }
137
- }
138
-
139
- console.log(chalk.green(`\n✔ Push complete. Pushed: ${pushCount}, Skipped: ${skipCount}`));
140
- } catch (err: any) {
141
- console.error(chalk.red(`Error: Push failed: ${err.message}`));
142
- process.exit(1);
143
- }
144
- }
145
-
146
- export async function pull(remoteTarget: string, localPath: string, options: { yes?: boolean }): Promise<void> {
147
- const forceYes = options.yes || false;
148
-
149
- try {
150
- const config = await loadConfig();
151
- const serverUrl = config.serverUrl;
152
- const apiKey = config.apiKey;
153
-
154
- if (!serverUrl || !apiKey) {
155
- console.error(chalk.red('Error: CLI not authenticated. Please run: hsync config set-server <url> && hsync login'));
156
- process.exit(1);
157
- }
158
-
159
- const remote = parseRemoteTarget(remoteTarget);
160
- const resolvedLocalPath = path.resolve(localPath);
161
-
162
- const spinner = ora('Fetching remote file tree...').start();
163
- const remoteClient = new RemoteClient(serverUrl, apiKey);
164
- const remoteFiles = await remoteClient.getTree(remote.slug, remote.remotePath);
165
- spinner.text = 'Scanning local files...';
166
-
167
- // Verify local path exists or create it
168
- await fs.mkdir(resolvedLocalPath, { recursive: true });
169
- const localFiles = await scanLocalPath(resolvedLocalPath, resolvedLocalPath);
170
- spinner.stop();
171
-
172
- console.log(chalk.cyan(`Found ${remoteFiles.length} remote files, ${localFiles.length} local files.`));
173
-
174
- const prompter = new OverwritePrompter(forceYes);
175
- let pullCount = 0;
176
- let skipCount = 0;
177
-
178
- for (const remoteFile of remoteFiles) {
179
- const localFile = localFiles.find((f) => f.relativePath === remoteFile.path);
180
- const localDestPath = path.join(resolvedLocalPath, remoteFile.path);
181
- const sourcePath = remote.remotePath
182
- ? `${remote.remotePath}/${remoteFile.path}`
183
- : remoteFile.path;
184
-
185
- let shouldDownload = false;
186
-
187
- if (!localFile) {
188
- // New remote file
189
- shouldDownload = true;
190
- } else if (localFile.hash !== remoteFile.hash) {
191
- // Conflict - content differs
192
- const confirm = await prompter.shouldOverwrite(remoteFile.path);
193
- if (confirm) {
194
- shouldDownload = true;
195
- } else {
196
- skipCount++;
197
- }
198
- } else {
199
- // Identical hashes
200
- skipCount++;
201
- }
202
-
203
- if (shouldDownload) {
204
- const fileSpinner = ora(`Pulling ${remoteFile.path}...`).start();
205
- try {
206
- await remoteClient.downloadFile(remote.slug, sourcePath, localDestPath);
207
- fileSpinner.succeed(chalk.green(`Pulled ${remoteFile.path}`));
208
- pullCount++;
209
- } catch (downloadErr: any) {
210
- fileSpinner.fail(chalk.red(`Failed to pull ${remoteFile.path}: ${downloadErr.message}`));
211
- }
212
- }
213
- }
214
-
215
- console.log(chalk.green(`\n✔ Pull complete. Pulled: ${pullCount}, Skipped: ${skipCount}`));
216
- } catch (err: any) {
217
- console.error(chalk.red(`Error: Pull failed: ${err.message}`));
218
- process.exit(1);
219
- }
220
- }
package/src/index.ts DELETED
@@ -1,126 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { Command } from 'commander';
3
- import { setServer, login, showMe, logout } from './commands/auth';
4
- import { listBuckets, createBucket, deleteBucket } from './commands/bucket';
5
- import { push, pull } from './commands/transfer';
6
- import { addSyncJob, listSyncJobs, removeSyncJob, startSyncScheduler } from './commands/sync';
7
-
8
- const program = new Command();
9
-
10
- program
11
- .name('hsync')
12
- .description('hsync CLI - Premium cross-device file synchronization client')
13
- .version('1.0.0');
14
-
15
- // === Auth Commands ===
16
- const configCmd = program.command('config').description('Configure CLI settings');
17
- configCmd
18
- .command('set-server <url>')
19
- .description('Set target API server URL')
20
- .action(async (url) => {
21
- await setServer(url);
22
- });
23
-
24
- program
25
- .command('login')
26
- .description('Authenticate and register local API Key')
27
- .option('-k, --api-key <key>', 'Skip interactive prompts and login directly using a pre-generated API Key')
28
- .action(async (options) => {
29
- await login(options);
30
- });
31
-
32
- program
33
- .command('me')
34
- .description('Show profile details of currently authenticated account')
35
- .action(async () => {
36
- await showMe();
37
- });
38
-
39
- program
40
- .command('logout')
41
- .description('De-authenticate and remove local API Key')
42
- .action(async () => {
43
- await logout();
44
- });
45
-
46
- // === Bucket Commands ===
47
- const bucketCmd = program.command('bucket').description('Manage remote storage buckets');
48
-
49
- bucketCmd
50
- .command('list')
51
- .description('List all storage buckets registered on your account')
52
- .action(async () => {
53
- await listBuckets();
54
- });
55
-
56
- bucketCmd
57
- .command('create <name> <slug>')
58
- .description('Create a new storage bucket')
59
- .requiredOption('-p, --provider <local|s3>', 'Storage provider type ("local" or "s3")')
60
- .option('-r, --rootPath <path>', 'Physical folder path on host (required for local provider)')
61
- .option('-b, --bucket <name>', 'S3 bucket name (required for s3 provider)')
62
- .action(async (name, slug, options) => {
63
- await createBucket(name, slug, options);
64
- });
65
-
66
- bucketCmd
67
- .command('delete <slug>')
68
- .description('Permanently delete a storage bucket')
69
- .action(async (slug) => {
70
- await deleteBucket(slug);
71
- });
72
-
73
- // === Transfer Commands ===
74
- program
75
- .command('push <localPath> <remoteTarget>')
76
- .description('Upload folder or file to a remote bucket')
77
- .option('-y, --yes', 'Automatically overwrite conflicts without interactive prompts')
78
- .action(async (localPath, remoteTarget, options) => {
79
- await push(localPath, remoteTarget, options);
80
- });
81
-
82
- program
83
- .command('pull <remoteTarget> <localPath>')
84
- .description('Download folder or file from a remote bucket')
85
- .option('-y, --yes', 'Automatically overwrite conflicts without interactive prompts')
86
- .action(async (remoteTarget, localPath, options) => {
87
- await pull(remoteTarget, localPath, options);
88
- });
89
-
90
- // === Sync Scheduler Commands ===
91
- const syncCmd = program.command('sync').description('Manage background synchronization schedules');
92
-
93
- syncCmd
94
- .command('add <localPath> <remoteTarget>')
95
- .description('Register a new persistent sync job profile')
96
- .requiredOption('-n, --name <jobName>', 'Unique identifier label for the sync job')
97
- .requiredOption('-d, --direction <push|pull|bidirectional>', 'Sync transfer direction mapping')
98
- .requiredOption('-s, --strategy <local-wins|server-wins|newer-wins>', 'Conflict resolution strategy rules')
99
- .option('-i, --interval <seconds>', 'Synchronization scheduling delay frequency in seconds', '10')
100
- .option('--no-conflict-copy', 'Disable keeping backup copies (.conflict-YYYY-MM-DD) on conflict resolutions')
101
- .action(async (localPath, remoteTarget, options) => {
102
- await addSyncJob(localPath, remoteTarget, options);
103
- });
104
-
105
- syncCmd
106
- .command('list')
107
- .description('List all registered local sync profiles')
108
- .action(async () => {
109
- await listSyncJobs();
110
- });
111
-
112
- syncCmd
113
- .command('remove <name>')
114
- .description('Remove a registered local sync job')
115
- .action(async (name) => {
116
- await removeSyncJob(name);
117
- });
118
-
119
- syncCmd
120
- .command('start [name]')
121
- .description('Start interval synchronization scheduler service')
122
- .action(async (name) => {
123
- await startSyncScheduler(name);
124
- });
125
-
126
- program.parse(process.argv);
package/src/types.ts DELETED
@@ -1,7 +0,0 @@
1
- import type { SyncJob } from '@file-sync/sync-engine';
2
-
3
- export interface CliConfig {
4
- serverUrl?: string;
5
- apiKey?: string;
6
- syncJobs?: SyncJob[];
7
- }
@@ -1,24 +0,0 @@
1
- import axios from 'axios';
2
- import type { AxiosInstance } from 'axios';
3
- import { loadConfig } from './config';
4
-
5
- export async function getHttpClient(): Promise<AxiosInstance> {
6
- const config = await loadConfig();
7
- const serverUrl = config.serverUrl;
8
- const apiKey = config.apiKey;
9
-
10
- if (!serverUrl) {
11
- throw new Error('Server URL is not set. Please run: hsync config set-server <url>');
12
- }
13
-
14
- const headers: Record<string, string> = {};
15
- if (apiKey) {
16
- headers['X-API-Key'] = apiKey;
17
- }
18
-
19
- return axios.create({
20
- baseURL: serverUrl,
21
- headers: headers,
22
- validateStatus: (status) => status >= 200 && status < 300,
23
- });
24
- }
@@ -1,25 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import os from 'os';
4
- import type { CliConfig } from '../types';
5
-
6
- const configDir = path.join(os.homedir(), '.hsync');
7
- const configPath = path.join(configDir, 'config.json');
8
-
9
- export async function loadConfig(): Promise<CliConfig> {
10
- try {
11
- const raw = await fs.readFile(configPath, 'utf-8');
12
- const parsed = JSON.parse(raw);
13
- if (!parsed.syncJobs) {
14
- parsed.syncJobs = [];
15
- }
16
- return parsed;
17
- } catch (err) {
18
- return { syncJobs: [] };
19
- }
20
- }
21
-
22
- export async function saveConfig(config: CliConfig): Promise<void> {
23
- await fs.mkdir(configDir, { recursive: true });
24
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
25
- }
@@ -1,51 +0,0 @@
1
- import inquirer from 'inquirer';
2
-
3
- export class OverwritePrompter {
4
- private overrideDecision: 'overwrite-all' | 'skip-all' | null = null;
5
- private forceYes: boolean;
6
-
7
- constructor(forceYes: boolean = false) {
8
- this.forceYes = forceYes;
9
- }
10
-
11
- async shouldOverwrite(filePath: string): Promise<boolean> {
12
- if (this.forceYes || this.overrideDecision === 'overwrite-all') {
13
- return true;
14
- }
15
- if (this.overrideDecision === 'skip-all') {
16
- return false;
17
- }
18
-
19
- const answer = await inquirer.prompt([
20
- {
21
- type: 'list',
22
- name: 'decision',
23
- message: `Conflict detected on "${filePath}". Local and remote content differ. How do you want to proceed?`,
24
- choices: [
25
- { name: 'Overwrite', value: 'overwrite' },
26
- { name: 'Skip', value: 'skip' },
27
- { name: 'Overwrite All', value: 'overwrite-all' },
28
- { name: 'Skip All', value: 'skip-all' },
29
- ],
30
- },
31
- ]);
32
-
33
- const choice = answer.decision;
34
- if (choice === 'overwrite') {
35
- return true;
36
- }
37
- if (choice === 'skip') {
38
- return false;
39
- }
40
- if (choice === 'overwrite-all') {
41
- this.overrideDecision = 'overwrite-all';
42
- return true;
43
- }
44
- if (choice === 'skip-all') {
45
- this.overrideDecision = 'skip-all';
46
- return false;
47
- }
48
-
49
- return false;
50
- }
51
- }
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src"
6
- },
7
- "include": ["src/**/*"]
8
- }