freestyle-sync 0.1.12 → 0.1.14

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/README.md CHANGED
@@ -22,6 +22,7 @@ import { gitAuthPlugin } from "@freestyle-sync/auth-git";
22
22
  import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
23
23
  import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
24
24
  import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
25
+ import { wranglerAuthPlugin } from "@freestyle-sync/auth-wrangler";
25
26
  import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
26
27
  import { nextjsPlugin } from "@freestyle-sync/nextjs";
27
28
  import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
@@ -37,6 +38,7 @@ export default defineConfig({
37
38
  githubCliAuthPlugin(),
38
39
  npmAuthPlugin(),
39
40
  yarnAuthPlugin(),
41
+ wranglerAuthPlugin(),
40
42
  dockerAuthPlugin(),
41
43
  awsAuthPlugin(),
42
44
  azureAuthPlugin(),
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";
@@ -48,6 +53,7 @@ const DEFAULT_CONFIG_DEPENDENCIES = [
48
53
  { name: "@freestyle-sync/auth-github-cli", spec: "@freestyle-sync/auth-github-cli@latest" },
49
54
  { name: "@freestyle-sync/auth-npm", spec: "@freestyle-sync/auth-npm@latest" },
50
55
  { name: "@freestyle-sync/auth-ssh", spec: "@freestyle-sync/auth-ssh@latest" },
56
+ { name: "@freestyle-sync/auth-wrangler", spec: "@freestyle-sync/auth-wrangler@latest" },
51
57
  { name: "@freestyle-sync/auth-yarn", spec: "@freestyle-sync/auth-yarn@latest" },
52
58
  { name: "@freestyle-sync/nextjs", spec: "@freestyle-sync/nextjs@latest" },
53
59
  { name: "@freestyle-sync/node-npm", spec: "@freestyle-sync/node-npm@latest" },
@@ -103,7 +109,12 @@ if (isDirectCliExecution()) {
103
109
  });
104
110
  }
105
111
  async function main() {
106
- const options = await parseArgs(process.argv.slice(2));
112
+ const args = process.argv.slice(2);
113
+ if (args[0] === "update") {
114
+ await runUpdateCommand(args.slice(1));
115
+ return;
116
+ }
117
+ const options = await parseArgs(args);
107
118
  const loadedConfig = await loadConfig(options.projectRoot);
108
119
  await sync({
109
120
  config: loadedConfig,
@@ -245,13 +256,12 @@ export async function sync(sdkOptions) {
245
256
  progress.step("Saving local sync cache");
246
257
  await saveCache(buildSyncCache());
247
258
  let snapshotPromise = null;
248
- const createSnapshot = vm.snapshot;
249
- if (options.snapshot && createSnapshot) {
259
+ if (options.snapshot && vm.snapshot) {
250
260
  progress.step(`Creating snapshot cache for ${vmId} in background`);
251
261
  snapshotPromise = (async () => {
252
262
  await runBeforeSnapshotPlugins(vm, vmId, options);
253
263
  try {
254
- const snapshot = await createSnapshot({ name: `${CLI_NAME}-${path.basename(options.projectRoot)}-${Date.now()}` });
264
+ const snapshot = await vm.snapshot({ name: `${CLI_NAME}-${path.basename(options.projectRoot)}-${Date.now()}` });
255
265
  return snapshot.snapshotId;
256
266
  }
257
267
  catch (error) {
@@ -378,6 +388,7 @@ import { gitAuthPlugin } from "@freestyle-sync/auth-git";
378
388
  import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
379
389
  import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
380
390
  import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
391
+ import { wranglerAuthPlugin } from "@freestyle-sync/auth-wrangler";
381
392
  import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
382
393
  import { nextjsPlugin } from "@freestyle-sync/nextjs";
383
394
  import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
@@ -394,6 +405,7 @@ export default defineConfig({
394
405
  githubCliAuthPlugin(),
395
406
  npmAuthPlugin(),
396
407
  yarnAuthPlugin(),
408
+ wranglerAuthPlugin(),
397
409
  dockerAuthPlugin(),
398
410
  awsAuthPlugin(),
399
411
  azureAuthPlugin(),
@@ -423,6 +435,7 @@ import { gitAuthPlugin } from "@freestyle-sync/auth-git";
423
435
  import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
424
436
  import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
425
437
  import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
438
+ import { wranglerAuthPlugin } from "@freestyle-sync/auth-wrangler";
426
439
  import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
427
440
  import { nextjsPlugin } from "@freestyle-sync/nextjs";
428
441
  import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
@@ -439,6 +452,7 @@ export default defineConfig({
439
452
  githubCliAuthPlugin(),
440
453
  npmAuthPlugin(),
441
454
  yarnAuthPlugin(),
455
+ wranglerAuthPlugin(),
442
456
  dockerAuthPlugin(),
443
457
  awsAuthPlugin(),
444
458
  azureAuthPlugin(),
@@ -480,16 +494,81 @@ async function ensureDefaultConfigDependencies(projectRoot) {
480
494
  throw new Error(`failed to install freestyle-sync config dependencies: ${details.trim() || String(error)}`);
481
495
  }
482
496
  }
497
+ async function updateDefaultConfigDependencies(projectRoot) {
498
+ const specs = DEFAULT_CONFIG_DEPENDENCIES.map((dependency) => dependency.spec);
499
+ const { command, args } = await dependencyInstallCommand(projectRoot, specs);
500
+ console.log(`Updating freestyle-sync packages with ${command}...`);
501
+ try {
502
+ await execFileAsync(command, args, { cwd: projectRoot });
503
+ }
504
+ catch (error) {
505
+ const details = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "") : String(error);
506
+ throw new Error(`failed to update freestyle-sync packages: ${details.trim() || String(error)}`);
507
+ }
508
+ console.log("Updated freestyle-sync and default plugins.");
509
+ }
483
510
  async function isDependencyInstalled(projectRoot, name) {
484
511
  return exists(path.join(projectRoot, "node_modules", ...name.split("/"), "package.json"));
485
512
  }
486
513
  async function dependencyInstallCommand(projectRoot, specs) {
487
- if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
514
+ const packageManager = await detectPackageManager(projectRoot);
515
+ if (packageManager === "bun")
516
+ return { command: "bun", args: ["add", "-d", ...specs] };
517
+ if (packageManager === "pnpm")
488
518
  return { command: "pnpm", args: ["add", "-D", ...specs] };
489
- if (await exists(path.join(projectRoot, "yarn.lock")))
519
+ if (packageManager === "yarn")
490
520
  return { command: "yarn", args: ["add", "-D", ...specs] };
491
521
  return { command: "npm", args: ["install", "--save-dev", ...specs] };
492
522
  }
523
+ async function detectPackageManager(projectRoot) {
524
+ if (await exists(path.join(projectRoot, "bun.lock")) || await exists(path.join(projectRoot, "bun.lockb")))
525
+ return "bun";
526
+ if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
527
+ return "pnpm";
528
+ if (await exists(path.join(projectRoot, "yarn.lock")))
529
+ return "yarn";
530
+ if (await exists(path.join(projectRoot, "package-lock.json")) || await exists(path.join(projectRoot, "npm-shrinkwrap.json")))
531
+ return "npm";
532
+ const packageManager = await readPackageManagerField(projectRoot);
533
+ if (packageManager?.startsWith("bun@"))
534
+ return "bun";
535
+ if (packageManager?.startsWith("pnpm@"))
536
+ return "pnpm";
537
+ if (packageManager?.startsWith("yarn@"))
538
+ return "yarn";
539
+ return "npm";
540
+ }
541
+ async function readPackageManagerField(projectRoot) {
542
+ try {
543
+ const parsed = JSON.parse(await readFile(path.join(projectRoot, "package.json"), "utf8"));
544
+ return typeof parsed.packageManager === "string" ? parsed.packageManager : undefined;
545
+ }
546
+ catch {
547
+ return undefined;
548
+ }
549
+ }
550
+ async function runUpdateCommand(args) {
551
+ const positional = [];
552
+ for (const arg of args) {
553
+ if (arg === "--help" || arg === "-h") {
554
+ printUpdateHelp();
555
+ return;
556
+ }
557
+ if (arg.startsWith("--")) {
558
+ throw new Error(`unknown update option: ${arg}`);
559
+ }
560
+ positional.push(arg);
561
+ }
562
+ if (positional.length > 1) {
563
+ throw new Error("expected at most one project path for update");
564
+ }
565
+ const projectRoot = path.resolve(positional[0] ?? process.cwd());
566
+ const projectStats = await stat(projectRoot).catch(() => null);
567
+ if (!projectStats?.isDirectory()) {
568
+ throw new Error(`project path is not a directory: ${projectRoot}`);
569
+ }
570
+ await updateDefaultConfigDependencies(projectRoot);
571
+ }
493
572
  async function parseArgs(args) {
494
573
  const options = defaultCliOptions();
495
574
  const positional = [];
@@ -636,6 +715,10 @@ function printHelp() {
636
715
 
637
716
  Usage:
638
717
  ${CLI_NAME} [project-dir] [options]
718
+ ${CLI_NAME} update [project-dir]
719
+
720
+ Commands:
721
+ update Update freestyle-sync and the default plugin packages.
639
722
 
640
723
  Options:
641
724
  --provider <name> Runtime provider: freestyle or apple-container. Defaults to freestyle.
@@ -666,6 +749,16 @@ Options:
666
749
  -h, --help Show this help.
667
750
  `);
668
751
  }
752
+ function printUpdateHelp() {
753
+ console.log(`${CLI_NAME} update refreshes freestyle-sync and the default plugin packages.
754
+
755
+ Usage:
756
+ ${CLI_NAME} update [project-dir]
757
+
758
+ The update command uses the project's package manager, including Bun when bun.lock,
759
+ bun.lockb, or packageManager: "bun@..." is present.
760
+ `);
761
+ }
669
762
  function readOptionValue(args, index, option) {
670
763
  const value = args[index];
671
764
  if (!value || value.startsWith("--")) {
@@ -1703,13 +1796,20 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1703
1796
  const canRenderInlineProgress = process.stdout.isTTY;
1704
1797
  const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
1705
1798
  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;
1799
+ const maxUploadConcurrency = Math.max(ARCHIVE_UPLOAD_MIN_CONCURRENCY, Math.min(ARCHIVE_UPLOAD_MAX_CONCURRENCY, chunkCount));
1800
+ let targetUploadConcurrency = Math.min(ARCHIVE_UPLOAD_INITIAL_CONCURRENCY, maxUploadConcurrency);
1801
+ let successfulUploadsSinceIncrease = 0;
1802
+ let uploadedChunks = 0;
1803
+ const inFlightUploads = new Set();
1804
+ const reportUploadedChunk = () => {
1805
+ uploadedChunks += 1;
1806
+ if (targetUploadConcurrency < maxUploadConcurrency) {
1807
+ successfulUploadsSinceIncrease += 1;
1808
+ if (successfulUploadsSinceIncrease >= targetUploadConcurrency) {
1809
+ targetUploadConcurrency += 1;
1810
+ successfulUploadsSinceIncrease = 0;
1811
+ }
1812
+ }
1713
1813
  if (chunkCount > 1) {
1714
1814
  const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
1715
1815
  if (canRenderInlineProgress) {
@@ -1722,7 +1822,38 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1722
1822
  console.log(progressMessage);
1723
1823
  }
1724
1824
  }
1825
+ };
1826
+ const waitForUploadedChunk = async () => {
1827
+ const result = await Promise.race(inFlightUploads);
1828
+ inFlightUploads.delete(result.upload);
1829
+ if (!result.ok) {
1830
+ throw result.error;
1831
+ }
1832
+ reportUploadedChunk();
1833
+ };
1834
+ const queueChunkUpload = (remotePath, content) => {
1835
+ let upload;
1836
+ upload = writeRemoteFileWithRetries(vm, remotePath, content, () => {
1837
+ targetUploadConcurrency = Math.max(ARCHIVE_UPLOAD_MIN_CONCURRENCY, Math.floor(targetUploadConcurrency / 2));
1838
+ successfulUploadsSinceIncrease = 0;
1839
+ })
1840
+ .then(() => ({ ok: true, upload }))
1841
+ .catch((error) => ({ ok: false, upload, error }));
1842
+ inFlightUploads.add(upload);
1843
+ };
1844
+ let index = 0;
1845
+ for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
1846
+ const chunkName = `${String(index).padStart(width, "0")}.chunk`;
1847
+ const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1848
+ chunkManifest.push(`${chunkName} ${content.length}`);
1849
+ queueChunkUpload(`${chunkDir}/${chunkName}`, content);
1725
1850
  index += 1;
1851
+ while (inFlightUploads.size >= targetUploadConcurrency) {
1852
+ await waitForUploadedChunk();
1853
+ }
1854
+ }
1855
+ while (inFlightUploads.size > 0) {
1856
+ await waitForUploadedChunk();
1726
1857
  }
1727
1858
  if (archiveSize > 0) {
1728
1859
  await vm.fs.writeTextFile(`${chunkDir}/manifest`, `${chunkManifest.join("\n")}\n`);
@@ -1793,6 +1924,21 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1793
1924
  `rm -rf ${shellQuote(chunkDir)}`,
1794
1925
  ].join("\n"), 5 * 60 * MS_PER_SECOND);
1795
1926
  }
1927
+ async function writeRemoteFileWithRetries(vm, remotePath, content, onRetry) {
1928
+ for (let attempt = 1; attempt <= ARCHIVE_UPLOAD_WRITE_ATTEMPTS; attempt += 1) {
1929
+ try {
1930
+ await vm.fs.writeFile(remotePath, content);
1931
+ return;
1932
+ }
1933
+ catch (error) {
1934
+ if (attempt >= ARCHIVE_UPLOAD_WRITE_ATTEMPTS) {
1935
+ throw error;
1936
+ }
1937
+ onRetry();
1938
+ await delay(ARCHIVE_UPLOAD_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
1939
+ }
1940
+ }
1941
+ }
1796
1942
  async function mkdirRemote(vm, directories) {
1797
1943
  for (const chunk of chunkArray(directories, 50)) {
1798
1944
  await checkedExec(vm, `mkdir -p ${chunk.map(shellQuote).join(" ")}`);
@@ -125,3 +125,5 @@ export type PushvmConfig = {
125
125
  };
126
126
  export declare function definePlugin(plugin: PushvmPlugin): PushvmPlugin;
127
127
  export declare function defineConfig(config: PushvmConfig): PushvmConfig;
128
+ export declare function localCommandOutput(execFileAsync: ExecFileAsync, file: string, args: string[]): Promise<string | undefined>;
129
+ export declare function runRemoteScript(vm: RemoteVm, utils: PushvmPluginUtils, name: string, script: string, timeoutMs?: number): Promise<void>;
@@ -4,3 +4,26 @@ export function definePlugin(plugin) {
4
4
  export function defineConfig(config) {
5
5
  return config;
6
6
  }
7
+ export async function localCommandOutput(execFileAsync, file, args) {
8
+ try {
9
+ const { stdout, stderr } = await execFileAsync(file, args);
10
+ return `${stdout}\n${stderr}`.trim();
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ export async function runRemoteScript(vm, utils, name, script, timeoutMs = 20 * 60 * 1000) {
17
+ const safeName = name.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
18
+ const scriptPath = `/tmp/freestyle-sync-${safeName}-${Date.now()}.sh`;
19
+ await vm.fs.writeTextFile(scriptPath, script);
20
+ const result = await vm.exec({
21
+ command: `bash ${utils.shellQuote(scriptPath)}\nexit_code=$?\nrm -f ${utils.shellQuote(scriptPath)}\nexit "$exit_code"`,
22
+ timeoutMs,
23
+ });
24
+ if (result.statusCode && result.statusCode !== 0) {
25
+ throw new Error(result.stderr ?? result.stdout ?? `${name} setup failed with status ${result.statusCode}`);
26
+ }
27
+ if (result.stdout?.trim())
28
+ console.log(result.stdout.trim());
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freestyle-sync",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "main": "dist/src/main.js",
6
6
  "exports": {