@anraktech/sync 0.1.0 → 0.2.0
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/cli.js +223 -35
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { createInterface } from "readline/promises";
|
|
6
|
-
import { stdin, stdout } from "process";
|
|
7
|
-
import { existsSync as
|
|
8
|
-
import { resolve } from "path";
|
|
9
|
-
import
|
|
5
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
6
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
7
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
8
|
+
import { resolve as resolve2 } from "path";
|
|
9
|
+
import chalk3 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -86,7 +86,7 @@ function openBrowser(url) {
|
|
|
86
86
|
exec(`${cmd} "${url}"`);
|
|
87
87
|
}
|
|
88
88
|
function findFreePort() {
|
|
89
|
-
return new Promise((
|
|
89
|
+
return new Promise((resolve3, reject) => {
|
|
90
90
|
const srv = createServer();
|
|
91
91
|
srv.listen(0, () => {
|
|
92
92
|
const addr = srv.address();
|
|
@@ -96,14 +96,14 @@ function findFreePort() {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
const port = addr.port;
|
|
99
|
-
srv.close(() =>
|
|
99
|
+
srv.close(() => resolve3(port));
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
async function browserLogin(apiUrl) {
|
|
104
104
|
const port = await findFreePort();
|
|
105
105
|
return new Promise(
|
|
106
|
-
(
|
|
106
|
+
(resolve3, reject) => {
|
|
107
107
|
let server;
|
|
108
108
|
const timeout = setTimeout(() => {
|
|
109
109
|
server?.close();
|
|
@@ -150,7 +150,7 @@ async function browserLogin(apiUrl) {
|
|
|
150
150
|
const tokens = await resp.json();
|
|
151
151
|
clearTimeout(timeout);
|
|
152
152
|
server.close();
|
|
153
|
-
|
|
153
|
+
resolve3(tokens);
|
|
154
154
|
} catch (err) {
|
|
155
155
|
clearTimeout(timeout);
|
|
156
156
|
server.close();
|
|
@@ -338,11 +338,11 @@ function persist() {
|
|
|
338
338
|
if (cache) saveCache(cache);
|
|
339
339
|
}
|
|
340
340
|
function hashFile(filePath) {
|
|
341
|
-
return new Promise((
|
|
341
|
+
return new Promise((resolve3, reject) => {
|
|
342
342
|
const hash = createHash("sha256");
|
|
343
343
|
const stream = createReadStream(filePath);
|
|
344
344
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
345
|
-
stream.on("end", () =>
|
|
345
|
+
stream.on("end", () => resolve3(hash.digest("hex")));
|
|
346
346
|
stream.on("error", reject);
|
|
347
347
|
});
|
|
348
348
|
}
|
|
@@ -416,8 +416,11 @@ function resetCache() {
|
|
|
416
416
|
|
|
417
417
|
// src/watcher.ts
|
|
418
418
|
import { watch } from "chokidar";
|
|
419
|
-
import { readdirSync, statSync } from "fs";
|
|
420
|
-
import { join as join2, relative as relative2, basename as basename4 } from "path";
|
|
419
|
+
import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
|
|
420
|
+
import { join as join2, relative as relative2, basename as basename4, resolve } from "path";
|
|
421
|
+
import { createInterface } from "readline/promises";
|
|
422
|
+
import { stdin, stdout } from "process";
|
|
423
|
+
import chalk2 from "chalk";
|
|
421
424
|
|
|
422
425
|
// src/uploader.ts
|
|
423
426
|
import { stat as stat2 } from "fs/promises";
|
|
@@ -664,6 +667,176 @@ async function pushSync(config) {
|
|
|
664
667
|
`Sync complete: ${result.uploaded} uploaded, ${result.failed} failed`
|
|
665
668
|
);
|
|
666
669
|
}
|
|
670
|
+
async function scanExternalFolder(config, folderPath, cases) {
|
|
671
|
+
const absPath = resolve(folderPath);
|
|
672
|
+
if (!existsSync2(absPath)) {
|
|
673
|
+
log.error(`Folder not found: ${absPath}`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (!statSync(absPath).isDirectory()) {
|
|
677
|
+
log.error(`Not a directory: ${absPath}`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
log.info(`Scanning ${absPath}...`);
|
|
681
|
+
let fileCount = 0;
|
|
682
|
+
function walk(dir) {
|
|
683
|
+
let entries;
|
|
684
|
+
try {
|
|
685
|
+
entries = readdirSync(dir);
|
|
686
|
+
} catch {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
for (const entry of entries) {
|
|
690
|
+
if (isIgnoredFile(entry)) continue;
|
|
691
|
+
const fullPath = join2(dir, entry);
|
|
692
|
+
let s;
|
|
693
|
+
try {
|
|
694
|
+
s = statSync(fullPath);
|
|
695
|
+
} catch {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (s.isDirectory()) {
|
|
699
|
+
walk(fullPath);
|
|
700
|
+
} else if (s.isFile() && isSupportedFile(entry)) {
|
|
701
|
+
fileCount++;
|
|
702
|
+
void enqueue(fullPath, join2(absPath, ".."));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
walk(absPath);
|
|
707
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
708
|
+
const queued = queueSize();
|
|
709
|
+
log.info(`Found ${fileCount} files, ${queued} need syncing`);
|
|
710
|
+
if (queued > 0) {
|
|
711
|
+
const result = await processQueue(config, cases);
|
|
712
|
+
log.info(`Synced: ${result.uploaded} uploaded, ${result.failed} failed`);
|
|
713
|
+
} else {
|
|
714
|
+
log.success("Everything up to date");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function startInteractiveAgent(config, getCases, refreshCases, triggerScan) {
|
|
718
|
+
const PROMPT = chalk2.blue("anrak> ");
|
|
719
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
720
|
+
function showHelp() {
|
|
721
|
+
console.log("");
|
|
722
|
+
console.log(chalk2.bold(" Available commands:"));
|
|
723
|
+
console.log("");
|
|
724
|
+
console.log(` ${chalk2.cyan("scan <folder>")} Scan a folder and sync its files to a case`);
|
|
725
|
+
console.log(` ${chalk2.cyan("rescan")} Re-scan the watch folder for new files`);
|
|
726
|
+
console.log(` ${chalk2.cyan("cases")} List cases on the server`);
|
|
727
|
+
console.log(` ${chalk2.cyan("status")} Show sync statistics`);
|
|
728
|
+
console.log(` ${chalk2.cyan("mappings")} Show folder \u2192 case mappings`);
|
|
729
|
+
console.log(` ${chalk2.cyan("refresh")} Refresh cases from server`);
|
|
730
|
+
console.log(` ${chalk2.cyan("help")} Show this help`);
|
|
731
|
+
console.log(` ${chalk2.cyan("quit")} Stop syncing and exit`);
|
|
732
|
+
console.log("");
|
|
733
|
+
}
|
|
734
|
+
async function handleCommand(input) {
|
|
735
|
+
const trimmed = input.trim();
|
|
736
|
+
if (!trimmed) return;
|
|
737
|
+
const [cmd, ...args] = trimmed.split(/\s+/);
|
|
738
|
+
const arg = args.join(" ");
|
|
739
|
+
switch (cmd.toLowerCase()) {
|
|
740
|
+
case "scan": {
|
|
741
|
+
if (!arg) {
|
|
742
|
+
log.warn("Usage: scan <folder path>");
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
await scanExternalFolder(config, arg, getCases());
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case "rescan": {
|
|
749
|
+
await triggerScan();
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "cases": {
|
|
753
|
+
await refreshCases();
|
|
754
|
+
const cases = getCases();
|
|
755
|
+
if (cases.length === 0) {
|
|
756
|
+
log.info("No cases found on server");
|
|
757
|
+
} else {
|
|
758
|
+
console.log("");
|
|
759
|
+
for (const c of cases) {
|
|
760
|
+
const docCount = c.documents?.length ?? 0;
|
|
761
|
+
console.log(
|
|
762
|
+
` ${chalk2.cyan(c.caseNumber)} ${c.caseName} ${chalk2.dim(`(${docCount} docs)`)}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
console.log("");
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "status": {
|
|
770
|
+
const stats = getStats();
|
|
771
|
+
console.log("");
|
|
772
|
+
console.log(` ${chalk2.bold("Watch folder:")} ${config.watchFolder}`);
|
|
773
|
+
console.log(` ${chalk2.bold("Files tracked:")} ${stats.totalFiles}`);
|
|
774
|
+
console.log(` ${chalk2.bold("Synced:")} ${chalk2.green(String(stats.synced))}`);
|
|
775
|
+
console.log(` ${chalk2.bold("Pending:")} ${chalk2.yellow(String(stats.pending))}`);
|
|
776
|
+
console.log(` ${chalk2.bold("Errors:")} ${chalk2.red(String(stats.errors))}`);
|
|
777
|
+
console.log(` ${chalk2.bold("Mapped folders:")} ${stats.mappedFolders}`);
|
|
778
|
+
console.log(` ${chalk2.bold("Queue:")} ${queueSize()}`);
|
|
779
|
+
console.log("");
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "mappings":
|
|
783
|
+
case "map": {
|
|
784
|
+
const mappings = getAllMappings();
|
|
785
|
+
const entries = Object.entries(mappings);
|
|
786
|
+
if (entries.length === 0) {
|
|
787
|
+
log.info("No folder mappings yet");
|
|
788
|
+
} else {
|
|
789
|
+
console.log("");
|
|
790
|
+
for (const [folder, m] of entries) {
|
|
791
|
+
console.log(
|
|
792
|
+
` ${chalk2.cyan(folder)} \u2192 ${m.caseNumber} ${chalk2.dim(`(${m.caseName})`)}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
console.log("");
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
case "refresh": {
|
|
800
|
+
log.info("Refreshing cases from server...");
|
|
801
|
+
await refreshCases();
|
|
802
|
+
log.success(`Found ${getCases().length} case(s)`);
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case "help":
|
|
806
|
+
case "?": {
|
|
807
|
+
showHelp();
|
|
808
|
+
break;
|
|
809
|
+
}
|
|
810
|
+
case "quit":
|
|
811
|
+
case "exit":
|
|
812
|
+
case "q": {
|
|
813
|
+
process.emit("SIGINT");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
default: {
|
|
817
|
+
log.warn(`Unknown command: ${cmd}. Type ${chalk2.cyan("help")} for available commands.`);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
showHelp();
|
|
823
|
+
async function promptLoop() {
|
|
824
|
+
while (true) {
|
|
825
|
+
let input;
|
|
826
|
+
try {
|
|
827
|
+
input = await rl.question(PROMPT);
|
|
828
|
+
} catch {
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
await handleCommand(input);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
void promptLoop();
|
|
839
|
+
}
|
|
667
840
|
async function startWatching(config) {
|
|
668
841
|
const folder = config.watchFolder;
|
|
669
842
|
log.info(`Scanning ${folder}...`);
|
|
@@ -684,7 +857,6 @@ async function startWatching(config) {
|
|
|
684
857
|
}
|
|
685
858
|
}, 5 * 60 * 1e3);
|
|
686
859
|
log.info(`Watching for changes...`);
|
|
687
|
-
log.dim("Press Ctrl+C to stop\n");
|
|
688
860
|
const watcher = watch(folder, {
|
|
689
861
|
ignored: /(^|[\/\\])(\.|~\$|Thumbs\.db|desktop\.ini)/,
|
|
690
862
|
persistent: true,
|
|
@@ -732,6 +904,22 @@ async function startWatching(config) {
|
|
|
732
904
|
watcher.on("error", (err) => {
|
|
733
905
|
log.error(`Watcher error: ${err instanceof Error ? err.message : String(err)}`);
|
|
734
906
|
});
|
|
907
|
+
startInteractiveAgent(
|
|
908
|
+
config,
|
|
909
|
+
() => cases,
|
|
910
|
+
async () => {
|
|
911
|
+
cases = await listCases(config);
|
|
912
|
+
},
|
|
913
|
+
async () => {
|
|
914
|
+
log.info(`Re-scanning ${folder}...`);
|
|
915
|
+
const result = await scanFolder(config);
|
|
916
|
+
log.info(`Scanned ${result.scanned} files, ${result.queued} need syncing`);
|
|
917
|
+
if (result.queued > 0) {
|
|
918
|
+
const syncResult = await processQueue(config, cases);
|
|
919
|
+
log.info(`Synced: ${syncResult.uploaded} uploaded, ${syncResult.failed} failed`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
);
|
|
735
923
|
const shutdown = () => {
|
|
736
924
|
log.info("Shutting down...");
|
|
737
925
|
clearInterval(refreshInterval);
|
|
@@ -749,11 +937,11 @@ async function startWatching(config) {
|
|
|
749
937
|
var program = new Command();
|
|
750
938
|
program.name("anrak-sync").description("AnrakLegal desktop file sync \u2014 watches local folders, syncs to case management").version("0.1.0");
|
|
751
939
|
program.command("init").description("Set up AnrakLegal Sync (first-time configuration)").option("--password", "Use email/password login instead of browser").action(async (opts) => {
|
|
752
|
-
const rl =
|
|
940
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
753
941
|
try {
|
|
754
|
-
console.log(
|
|
942
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Setup\n"));
|
|
755
943
|
const apiUrl = await rl.question(
|
|
756
|
-
` AnrakLegal URL ${
|
|
944
|
+
` AnrakLegal URL ${chalk3.dim("(https://anrak.legal)")}: `
|
|
757
945
|
) || "https://anrak.legal";
|
|
758
946
|
log.info("Connecting to server...");
|
|
759
947
|
const serverConfig = await fetchServerConfig(apiUrl);
|
|
@@ -781,14 +969,14 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
|
|
|
781
969
|
tokens = await browserLogin(apiUrl);
|
|
782
970
|
}
|
|
783
971
|
log.success("Authenticated");
|
|
784
|
-
const rl2 =
|
|
972
|
+
const rl2 = createInterface2({ input: stdin2, output: stdout2 });
|
|
785
973
|
const defaultFolder = process.platform === "win32" ? "C:\\Cases" : `${process.env.HOME}/Cases`;
|
|
786
974
|
const watchInput = await rl2.question(
|
|
787
|
-
` Watch folder ${
|
|
975
|
+
` Watch folder ${chalk3.dim(`(${defaultFolder})`)}: `
|
|
788
976
|
);
|
|
789
|
-
const watchFolder =
|
|
977
|
+
const watchFolder = resolve2(watchInput || defaultFolder);
|
|
790
978
|
rl2.close();
|
|
791
|
-
if (!
|
|
979
|
+
if (!existsSync3(watchFolder)) {
|
|
792
980
|
log.warn(
|
|
793
981
|
`Folder ${watchFolder} does not exist \u2014 it will be created when you add files`
|
|
794
982
|
);
|
|
@@ -810,7 +998,7 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
|
|
|
810
998
|
log.info(`Config saved to ${getConfigDir()}`);
|
|
811
999
|
log.info(`Watching: ${watchFolder}`);
|
|
812
1000
|
console.log(
|
|
813
|
-
|
|
1001
|
+
chalk3.dim("\n Run `anrak-sync start` to begin syncing\n")
|
|
814
1002
|
);
|
|
815
1003
|
} catch (err) {
|
|
816
1004
|
log.error(err instanceof Error ? err.message : String(err));
|
|
@@ -822,7 +1010,7 @@ program.command("login").description("Re-authenticate with AnrakLegal").option("
|
|
|
822
1010
|
try {
|
|
823
1011
|
let tokens;
|
|
824
1012
|
if (opts.password) {
|
|
825
|
-
const rl =
|
|
1013
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
826
1014
|
try {
|
|
827
1015
|
const email = await rl.question(" Email: ");
|
|
828
1016
|
const password = await rl.question(" Password: ");
|
|
@@ -845,7 +1033,7 @@ program.command("login").description("Re-authenticate with AnrakLegal").option("
|
|
|
845
1033
|
});
|
|
846
1034
|
program.command("start").description("Start watching for file changes and syncing").action(async () => {
|
|
847
1035
|
const config = requireConfig();
|
|
848
|
-
console.log(
|
|
1036
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync\n"));
|
|
849
1037
|
log.info(`Watching: ${config.watchFolder}`);
|
|
850
1038
|
log.info(`Server: ${config.apiUrl}`);
|
|
851
1039
|
console.log("");
|
|
@@ -858,7 +1046,7 @@ program.command("start").description("Start watching for file changes and syncin
|
|
|
858
1046
|
});
|
|
859
1047
|
program.command("push").description("One-time sync \u2014 upload all new/changed files, then exit").action(async () => {
|
|
860
1048
|
const config = requireConfig();
|
|
861
|
-
console.log(
|
|
1049
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Push\n"));
|
|
862
1050
|
log.info(`Folder: ${config.watchFolder}`);
|
|
863
1051
|
log.info(`Server: ${config.apiUrl}`);
|
|
864
1052
|
console.log("");
|
|
@@ -872,24 +1060,24 @@ program.command("push").description("One-time sync \u2014 upload all new/changed
|
|
|
872
1060
|
program.command("status").description("Show sync status").action(async () => {
|
|
873
1061
|
const config = requireConfig();
|
|
874
1062
|
const stats = getStats();
|
|
875
|
-
console.log(
|
|
1063
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Status\n"));
|
|
876
1064
|
console.log(` Server: ${config.apiUrl}`);
|
|
877
1065
|
console.log(` Watch folder: ${config.watchFolder}`);
|
|
878
1066
|
console.log(` Config: ${getConfigDir()}`);
|
|
879
1067
|
console.log("");
|
|
880
1068
|
console.log(` Files tracked: ${stats.totalFiles}`);
|
|
881
|
-
console.log(` Synced: ${
|
|
882
|
-
console.log(` Pending: ${
|
|
883
|
-
console.log(` Errors: ${
|
|
1069
|
+
console.log(` Synced: ${chalk3.green(stats.synced)}`);
|
|
1070
|
+
console.log(` Pending: ${chalk3.yellow(stats.pending)}`);
|
|
1071
|
+
console.log(` Errors: ${chalk3.red(stats.errors)}`);
|
|
884
1072
|
console.log(` Mapped folders: ${stats.mappedFolders}`);
|
|
885
1073
|
try {
|
|
886
1074
|
const cases = await listCases(config);
|
|
887
1075
|
console.log(`
|
|
888
1076
|
Server cases: ${cases.length}`);
|
|
889
|
-
console.log(` Auth: ${
|
|
1077
|
+
console.log(` Auth: ${chalk3.green("valid")}`);
|
|
890
1078
|
} catch {
|
|
891
1079
|
console.log(`
|
|
892
|
-
Auth: ${
|
|
1080
|
+
Auth: ${chalk3.red("expired \u2014 run anrak-sync login")}`);
|
|
893
1081
|
}
|
|
894
1082
|
console.log("");
|
|
895
1083
|
});
|
|
@@ -897,13 +1085,13 @@ program.command("map").description("Show folder-to-case mappings").action(async
|
|
|
897
1085
|
const config = requireConfig();
|
|
898
1086
|
const mappings = getAllMappings();
|
|
899
1087
|
const entries = Object.entries(mappings);
|
|
900
|
-
console.log(
|
|
1088
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Mappings\n"));
|
|
901
1089
|
if (entries.length === 0) {
|
|
902
1090
|
log.info("No mappings yet. Run `anrak-sync push` or `anrak-sync start` to create them.");
|
|
903
1091
|
} else {
|
|
904
1092
|
for (const [folder, mapping] of entries) {
|
|
905
1093
|
console.log(
|
|
906
|
-
` ${
|
|
1094
|
+
` ${chalk3.cyan(folder)} -> ${mapping.caseNumber} (${chalk3.dim(mapping.caseName)})`
|
|
907
1095
|
);
|
|
908
1096
|
}
|
|
909
1097
|
}
|
|
@@ -912,9 +1100,9 @@ program.command("map").description("Show folder-to-case mappings").action(async
|
|
|
912
1100
|
const mappedIds = new Set(entries.map(([, m]) => m.caseId));
|
|
913
1101
|
const unmapped = cases.filter((c) => !mappedIds.has(c.id));
|
|
914
1102
|
if (unmapped.length > 0) {
|
|
915
|
-
console.log(
|
|
1103
|
+
console.log(chalk3.dim("\n Unmapped server cases:"));
|
|
916
1104
|
for (const c of unmapped) {
|
|
917
|
-
console.log(` ${
|
|
1105
|
+
console.log(` ${chalk3.dim(c.caseNumber)} ${chalk3.dim(c.caseName)}`);
|
|
918
1106
|
}
|
|
919
1107
|
}
|
|
920
1108
|
} catch {
|