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 +282 -6
- package/package.json +5 -2
- package/dist/hsync +0 -0
- package/scripts/build-js.ts +0 -32
- package/src/commands/auth.ts +0 -139
- package/src/commands/bucket.ts +0 -110
- package/src/commands/sync.ts +0 -213
- package/src/commands/transfer.ts +0 -220
- package/src/index.ts +0 -126
- package/src/types.ts +0 -7
- package/src/utils/client.ts +0 -24
- package/src/utils/config.ts +0 -25
- package/src/utils/prompts.ts +0 -51
- package/tsconfig.json +0 -8
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 {
|
|
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.
|
|
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
|
package/scripts/build-js.ts
DELETED
|
@@ -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();
|
package/src/commands/auth.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/bucket.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/sync.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/transfer.ts
DELETED
|
@@ -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
package/src/utils/client.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/config.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/prompts.ts
DELETED
|
@@ -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
|
-
}
|