@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.
Files changed (2) hide show
  1. package/dist/cli.js +223 -35
  2. 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 existsSync2, statSync as statSync2 } from "fs";
8
- import { resolve } from "path";
9
- import chalk2 from "chalk";
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((resolve2, reject) => {
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(() => resolve2(port));
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
- (resolve2, reject) => {
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
- resolve2(tokens);
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((resolve2, reject) => {
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", () => resolve2(hash.digest("hex")));
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 = createInterface({ input: stdin, output: stdout });
940
+ const rl = createInterface2({ input: stdin2, output: stdout2 });
753
941
  try {
754
- console.log(chalk2.bold.blue("\n AnrakLegal Sync \u2014 Setup\n"));
942
+ console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Setup\n"));
755
943
  const apiUrl = await rl.question(
756
- ` AnrakLegal URL ${chalk2.dim("(https://anrak.legal)")}: `
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 = createInterface({ input: stdin, output: stdout });
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 ${chalk2.dim(`(${defaultFolder})`)}: `
975
+ ` Watch folder ${chalk3.dim(`(${defaultFolder})`)}: `
788
976
  );
789
- const watchFolder = resolve(watchInput || defaultFolder);
977
+ const watchFolder = resolve2(watchInput || defaultFolder);
790
978
  rl2.close();
791
- if (!existsSync2(watchFolder)) {
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
- chalk2.dim("\n Run `anrak-sync start` to begin syncing\n")
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 = createInterface({ input: stdin, output: stdout });
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(chalk2.bold.blue("\n AnrakLegal Sync\n"));
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(chalk2.bold.blue("\n AnrakLegal Sync \u2014 Push\n"));
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(chalk2.bold.blue("\n AnrakLegal Sync \u2014 Status\n"));
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: ${chalk2.green(stats.synced)}`);
882
- console.log(` Pending: ${chalk2.yellow(stats.pending)}`);
883
- console.log(` Errors: ${chalk2.red(stats.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: ${chalk2.green("valid")}`);
1077
+ console.log(` Auth: ${chalk3.green("valid")}`);
890
1078
  } catch {
891
1079
  console.log(`
892
- Auth: ${chalk2.red("expired \u2014 run anrak-sync login")}`);
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(chalk2.bold.blue("\n AnrakLegal Sync \u2014 Mappings\n"));
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
- ` ${chalk2.cyan(folder)} -> ${mapping.caseNumber} (${chalk2.dim(mapping.caseName)})`
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(chalk2.dim("\n Unmapped server cases:"));
1103
+ console.log(chalk3.dim("\n Unmapped server cases:"));
916
1104
  for (const c of unmapped) {
917
- console.log(` ${chalk2.dim(c.caseNumber)} ${chalk2.dim(c.caseName)}`);
1105
+ console.log(` ${chalk3.dim(c.caseNumber)} ${chalk3.dim(c.caseName)}`);
918
1106
  }
919
1107
  }
920
1108
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anraktech/sync",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AnrakLegal desktop file sync agent — watches local folders and syncs to case management",
5
5
  "type": "module",
6
6
  "bin": {