freestyle-sync 0.1.12 → 0.1.13

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/src/main.js +152 -10
  2. package/package.json +1 -1
package/dist/src/main.js CHANGED
@@ -23,6 +23,11 @@ const CLI_NAME = "freestyle-sync";
23
23
  const CACHE_VERSION = 1;
24
24
  const PLUGIN_PREFERENCES_VERSION = 1;
25
25
  const ARCHIVE_CHUNK_BYTES = 512 * 1024;
26
+ const ARCHIVE_UPLOAD_MIN_CONCURRENCY = 1;
27
+ const ARCHIVE_UPLOAD_INITIAL_CONCURRENCY = 4;
28
+ const ARCHIVE_UPLOAD_MAX_CONCURRENCY = 16;
29
+ const ARCHIVE_UPLOAD_WRITE_ATTEMPTS = 3;
30
+ const ARCHIVE_UPLOAD_RETRY_BASE_DELAY_MS = 200;
26
31
  const MS_PER_SECOND = 1000;
27
32
  const DEFAULT_APPLE_CONTAINER_IMAGE = "ubuntu:24.04";
28
33
  const DEFAULT_FREESTYLE_API_URL = "https://api.freestyle.sh";
@@ -103,7 +108,12 @@ if (isDirectCliExecution()) {
103
108
  });
104
109
  }
105
110
  async function main() {
106
- const options = await parseArgs(process.argv.slice(2));
111
+ const args = process.argv.slice(2);
112
+ if (args[0] === "update") {
113
+ await runUpdateCommand(args.slice(1));
114
+ return;
115
+ }
116
+ const options = await parseArgs(args);
107
117
  const loadedConfig = await loadConfig(options.projectRoot);
108
118
  await sync({
109
119
  config: loadedConfig,
@@ -480,16 +490,81 @@ async function ensureDefaultConfigDependencies(projectRoot) {
480
490
  throw new Error(`failed to install freestyle-sync config dependencies: ${details.trim() || String(error)}`);
481
491
  }
482
492
  }
493
+ async function updateDefaultConfigDependencies(projectRoot) {
494
+ const specs = DEFAULT_CONFIG_DEPENDENCIES.map((dependency) => dependency.spec);
495
+ const { command, args } = await dependencyInstallCommand(projectRoot, specs);
496
+ console.log(`Updating freestyle-sync packages with ${command}...`);
497
+ try {
498
+ await execFileAsync(command, args, { cwd: projectRoot });
499
+ }
500
+ catch (error) {
501
+ const details = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "") : String(error);
502
+ throw new Error(`failed to update freestyle-sync packages: ${details.trim() || String(error)}`);
503
+ }
504
+ console.log("Updated freestyle-sync and default plugins.");
505
+ }
483
506
  async function isDependencyInstalled(projectRoot, name) {
484
507
  return exists(path.join(projectRoot, "node_modules", ...name.split("/"), "package.json"));
485
508
  }
486
509
  async function dependencyInstallCommand(projectRoot, specs) {
487
- if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
510
+ const packageManager = await detectPackageManager(projectRoot);
511
+ if (packageManager === "bun")
512
+ return { command: "bun", args: ["add", "-d", ...specs] };
513
+ if (packageManager === "pnpm")
488
514
  return { command: "pnpm", args: ["add", "-D", ...specs] };
489
- if (await exists(path.join(projectRoot, "yarn.lock")))
515
+ if (packageManager === "yarn")
490
516
  return { command: "yarn", args: ["add", "-D", ...specs] };
491
517
  return { command: "npm", args: ["install", "--save-dev", ...specs] };
492
518
  }
519
+ async function detectPackageManager(projectRoot) {
520
+ if (await exists(path.join(projectRoot, "bun.lock")) || await exists(path.join(projectRoot, "bun.lockb")))
521
+ return "bun";
522
+ if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
523
+ return "pnpm";
524
+ if (await exists(path.join(projectRoot, "yarn.lock")))
525
+ return "yarn";
526
+ if (await exists(path.join(projectRoot, "package-lock.json")) || await exists(path.join(projectRoot, "npm-shrinkwrap.json")))
527
+ return "npm";
528
+ const packageManager = await readPackageManagerField(projectRoot);
529
+ if (packageManager?.startsWith("bun@"))
530
+ return "bun";
531
+ if (packageManager?.startsWith("pnpm@"))
532
+ return "pnpm";
533
+ if (packageManager?.startsWith("yarn@"))
534
+ return "yarn";
535
+ return "npm";
536
+ }
537
+ async function readPackageManagerField(projectRoot) {
538
+ try {
539
+ const parsed = JSON.parse(await readFile(path.join(projectRoot, "package.json"), "utf8"));
540
+ return typeof parsed.packageManager === "string" ? parsed.packageManager : undefined;
541
+ }
542
+ catch {
543
+ return undefined;
544
+ }
545
+ }
546
+ async function runUpdateCommand(args) {
547
+ const positional = [];
548
+ for (const arg of args) {
549
+ if (arg === "--help" || arg === "-h") {
550
+ printUpdateHelp();
551
+ return;
552
+ }
553
+ if (arg.startsWith("--")) {
554
+ throw new Error(`unknown update option: ${arg}`);
555
+ }
556
+ positional.push(arg);
557
+ }
558
+ if (positional.length > 1) {
559
+ throw new Error("expected at most one project path for update");
560
+ }
561
+ const projectRoot = path.resolve(positional[0] ?? process.cwd());
562
+ const projectStats = await stat(projectRoot).catch(() => null);
563
+ if (!projectStats?.isDirectory()) {
564
+ throw new Error(`project path is not a directory: ${projectRoot}`);
565
+ }
566
+ await updateDefaultConfigDependencies(projectRoot);
567
+ }
493
568
  async function parseArgs(args) {
494
569
  const options = defaultCliOptions();
495
570
  const positional = [];
@@ -636,6 +711,10 @@ function printHelp() {
636
711
 
637
712
  Usage:
638
713
  ${CLI_NAME} [project-dir] [options]
714
+ ${CLI_NAME} update [project-dir]
715
+
716
+ Commands:
717
+ update Update freestyle-sync and the default plugin packages.
639
718
 
640
719
  Options:
641
720
  --provider <name> Runtime provider: freestyle or apple-container. Defaults to freestyle.
@@ -666,6 +745,16 @@ Options:
666
745
  -h, --help Show this help.
667
746
  `);
668
747
  }
748
+ function printUpdateHelp() {
749
+ console.log(`${CLI_NAME} update refreshes freestyle-sync and the default plugin packages.
750
+
751
+ Usage:
752
+ ${CLI_NAME} update [project-dir]
753
+
754
+ The update command uses the project's package manager, including Bun when bun.lock,
755
+ bun.lockb, or packageManager: "bun@..." is present.
756
+ `);
757
+ }
669
758
  function readOptionValue(args, index, option) {
670
759
  const value = args[index];
671
760
  if (!value || value.startsWith("--")) {
@@ -1703,13 +1792,20 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1703
1792
  const canRenderInlineProgress = process.stdout.isTTY;
1704
1793
  const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
1705
1794
  const chunkManifest = [];
1706
- let index = 0;
1707
- for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
1708
- const chunkName = `${String(index).padStart(width, "0")}.chunk`;
1709
- const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1710
- chunkManifest.push(`${chunkName} ${content.length}`);
1711
- await vm.fs.writeFile(`${chunkDir}/${chunkName}`, content);
1712
- const uploadedChunks = index + 1;
1795
+ const maxUploadConcurrency = Math.max(ARCHIVE_UPLOAD_MIN_CONCURRENCY, Math.min(ARCHIVE_UPLOAD_MAX_CONCURRENCY, chunkCount));
1796
+ let targetUploadConcurrency = Math.min(ARCHIVE_UPLOAD_INITIAL_CONCURRENCY, maxUploadConcurrency);
1797
+ let successfulUploadsSinceIncrease = 0;
1798
+ let uploadedChunks = 0;
1799
+ const inFlightUploads = new Set();
1800
+ const reportUploadedChunk = () => {
1801
+ uploadedChunks += 1;
1802
+ if (targetUploadConcurrency < maxUploadConcurrency) {
1803
+ successfulUploadsSinceIncrease += 1;
1804
+ if (successfulUploadsSinceIncrease >= targetUploadConcurrency) {
1805
+ targetUploadConcurrency += 1;
1806
+ successfulUploadsSinceIncrease = 0;
1807
+ }
1808
+ }
1713
1809
  if (chunkCount > 1) {
1714
1810
  const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
1715
1811
  if (canRenderInlineProgress) {
@@ -1722,7 +1818,38 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1722
1818
  console.log(progressMessage);
1723
1819
  }
1724
1820
  }
1821
+ };
1822
+ const waitForUploadedChunk = async () => {
1823
+ const result = await Promise.race(inFlightUploads);
1824
+ inFlightUploads.delete(result.upload);
1825
+ if (!result.ok) {
1826
+ throw result.error;
1827
+ }
1828
+ reportUploadedChunk();
1829
+ };
1830
+ const queueChunkUpload = (remotePath, content) => {
1831
+ let upload;
1832
+ upload = writeRemoteFileWithRetries(vm, remotePath, content, () => {
1833
+ targetUploadConcurrency = Math.max(ARCHIVE_UPLOAD_MIN_CONCURRENCY, Math.floor(targetUploadConcurrency / 2));
1834
+ successfulUploadsSinceIncrease = 0;
1835
+ })
1836
+ .then(() => ({ ok: true, upload }))
1837
+ .catch((error) => ({ ok: false, upload, error }));
1838
+ inFlightUploads.add(upload);
1839
+ };
1840
+ let index = 0;
1841
+ for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
1842
+ const chunkName = `${String(index).padStart(width, "0")}.chunk`;
1843
+ const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1844
+ chunkManifest.push(`${chunkName} ${content.length}`);
1845
+ queueChunkUpload(`${chunkDir}/${chunkName}`, content);
1725
1846
  index += 1;
1847
+ while (inFlightUploads.size >= targetUploadConcurrency) {
1848
+ await waitForUploadedChunk();
1849
+ }
1850
+ }
1851
+ while (inFlightUploads.size > 0) {
1852
+ await waitForUploadedChunk();
1726
1853
  }
1727
1854
  if (archiveSize > 0) {
1728
1855
  await vm.fs.writeTextFile(`${chunkDir}/manifest`, `${chunkManifest.join("\n")}\n`);
@@ -1793,6 +1920,21 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1793
1920
  `rm -rf ${shellQuote(chunkDir)}`,
1794
1921
  ].join("\n"), 5 * 60 * MS_PER_SECOND);
1795
1922
  }
1923
+ async function writeRemoteFileWithRetries(vm, remotePath, content, onRetry) {
1924
+ for (let attempt = 1; attempt <= ARCHIVE_UPLOAD_WRITE_ATTEMPTS; attempt += 1) {
1925
+ try {
1926
+ await vm.fs.writeFile(remotePath, content);
1927
+ return;
1928
+ }
1929
+ catch (error) {
1930
+ if (attempt >= ARCHIVE_UPLOAD_WRITE_ATTEMPTS) {
1931
+ throw error;
1932
+ }
1933
+ onRetry();
1934
+ await delay(ARCHIVE_UPLOAD_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
1935
+ }
1936
+ }
1937
+ }
1796
1938
  async function mkdirRemote(vm, directories) {
1797
1939
  for (const chunk of chunkArray(directories, 50)) {
1798
1940
  await checkedExec(vm, `mkdir -p ${chunk.map(shellQuote).join(" ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freestyle-sync",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "main": "dist/src/main.js",
6
6
  "exports": {