freestyle-sync 0.1.11 → 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 +191 -12
  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";
@@ -30,6 +35,8 @@ const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
30
35
  const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
31
36
  const DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY = "pck_h2aft7g9pqjzrkdnzs199h1may5wjtdtdxeex7m2wzp1r";
32
37
  const STACK_REFRESH_TOKEN_ENV_KEY = "FREESTYLE_STACK_REFRESH_TOKEN";
38
+ const GENERATED_UPLOAD_DIRECTORY_NAMES = new Set([".cache", ".freestyle-sync", ".git", ".next", ".turbo", ".vercel", ".vite", "build", "coverage", "dist", "node_modules", "target"]);
39
+ const DEFAULT_PROJECT_EXCLUDES = [".freestyle-sync"];
33
40
  const USE_UNICODE_OUTPUT = process.stdout.isTTY && (process.env.TERM !== "dumb" || Boolean(process.env.TERM_PROGRAM));
34
41
  const USE_STYLED_OUTPUT = process.stdout.isTTY && process.env.NO_COLOR !== "1";
35
42
  const DEFAULT_CONFIG_DEPENDENCIES = [
@@ -101,7 +108,12 @@ if (isDirectCliExecution()) {
101
108
  });
102
109
  }
103
110
  async function main() {
104
- 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);
105
117
  const loadedConfig = await loadConfig(options.projectRoot);
106
118
  await sync({
107
119
  config: loadedConfig,
@@ -110,7 +122,6 @@ async function main() {
110
122
  }
111
123
  export async function sync(sdkOptions) {
112
124
  const options = await resolveCliOptions(sdkOptions.options);
113
- printHeading(CLI_NAME);
114
125
  config = sdkOptions.config;
115
126
  plugins = config.plugins;
116
127
  const pluginPreferences = await updatePluginPreferences(options, sdkOptions.pluginPreferences);
@@ -479,16 +490,81 @@ async function ensureDefaultConfigDependencies(projectRoot) {
479
490
  throw new Error(`failed to install freestyle-sync config dependencies: ${details.trim() || String(error)}`);
480
491
  }
481
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
+ }
482
506
  async function isDependencyInstalled(projectRoot, name) {
483
507
  return exists(path.join(projectRoot, "node_modules", ...name.split("/"), "package.json"));
484
508
  }
485
509
  async function dependencyInstallCommand(projectRoot, specs) {
486
- 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")
487
514
  return { command: "pnpm", args: ["add", "-D", ...specs] };
488
- if (await exists(path.join(projectRoot, "yarn.lock")))
515
+ if (packageManager === "yarn")
489
516
  return { command: "yarn", args: ["add", "-D", ...specs] };
490
517
  return { command: "npm", args: ["install", "--save-dev", ...specs] };
491
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
+ }
492
568
  async function parseArgs(args) {
493
569
  const options = defaultCliOptions();
494
570
  const positional = [];
@@ -635,6 +711,10 @@ function printHelp() {
635
711
 
636
712
  Usage:
637
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.
638
718
 
639
719
  Options:
640
720
  --provider <name> Runtime provider: freestyle or apple-container. Defaults to freestyle.
@@ -665,6 +745,16 @@ Options:
665
745
  -h, --help Show this help.
666
746
  `);
667
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
+ }
668
758
  function readOptionValue(args, index, option) {
669
759
  const value = args[index];
670
760
  if (!value || value.startsWith("--")) {
@@ -745,7 +835,7 @@ async function resolveProjectSyncConfig(options) {
745
835
  configs.push(pluginConfig);
746
836
  }
747
837
  return {
748
- exclude: configs.flatMap((projectConfig) => projectConfig.exclude ?? []).map(normalizeProjectPattern),
838
+ exclude: [...DEFAULT_PROJECT_EXCLUDES, ...configs.flatMap((projectConfig) => projectConfig.exclude ?? [])].map(normalizeProjectPattern),
749
839
  include: configs.flatMap((projectConfig) => normalizeProjectIncludes(options.projectRoot, projectConfig.include ?? [])),
750
840
  };
751
841
  }
@@ -1081,6 +1171,8 @@ function printPlan(options, projectChanges, contextChanges, envExports, cache) {
1081
1171
  console.log(`${dim(" Project files:")} ${projectChanges.changed.length} changed, ${projectChanges.removed.length} removed, ${projectChanges.unchanged} unchanged`);
1082
1172
  console.log(`${dim(" Context files:")} ${contextChanges.changed.length} changed, ${contextChanges.removed.length} removed, ${contextChanges.unchanged} unchanged`);
1083
1173
  console.log(`${dim(" Estimated upload:")} ${formatBytes(totalEntrySize(projectChanges.changed))} project, ${formatBytes(totalEntrySize(contextChanges.changed))} context`);
1174
+ printUploadBreakdown("Project upload contributors", projectUploadContributors(projectChanges.changed), "consider sync.exclude for generated folders");
1175
+ printUploadBreakdown("Context upload contributors", contextUploadContributors(contextChanges.changed), "disable unneeded context plugins to skip");
1084
1176
  if (Object.keys(envExports).length > 0) {
1085
1177
  console.log(`${dim(" Environment exports:")} ${Object.keys(envExports).length}`);
1086
1178
  }
@@ -1089,6 +1181,40 @@ function printPlan(options, projectChanges, contextChanges, envExports, cache) {
1089
1181
  function totalEntrySize(entries) {
1090
1182
  return entries.reduce((total, entry) => total + entry.size, 0);
1091
1183
  }
1184
+ function projectUploadContributors(entries) {
1185
+ return largestContributors(entries, (entry) => contributionPath(entry.relativePath));
1186
+ }
1187
+ function contextUploadContributors(entries) {
1188
+ return largestContributors(entries, (entry) => entry.label);
1189
+ }
1190
+ function contributionPath(relativePath) {
1191
+ const parts = relativePath.split("/").filter(Boolean);
1192
+ if (parts.length <= 1)
1193
+ return relativePath;
1194
+ const generatedDirectoryIndex = parts.findIndex((part) => GENERATED_UPLOAD_DIRECTORY_NAMES.has(part));
1195
+ if (generatedDirectoryIndex >= 0)
1196
+ return parts.slice(0, generatedDirectoryIndex + 1).join("/");
1197
+ return parts.slice(0, Math.min(parts.length - 1, 3)).join("/");
1198
+ }
1199
+ function largestContributors(entries, bucketForEntry) {
1200
+ const buckets = new Map();
1201
+ for (const entry of entries) {
1202
+ const bucketName = bucketForEntry(entry);
1203
+ const bucket = buckets.get(bucketName) ?? { path: bucketName, size: 0, files: 0 };
1204
+ bucket.size += entry.size;
1205
+ bucket.files += 1;
1206
+ buckets.set(bucketName, bucket);
1207
+ }
1208
+ return Array.from(buckets.values()).sort((left, right) => right.size - left.size).slice(0, 6);
1209
+ }
1210
+ function printUploadBreakdown(title, contributors, hint) {
1211
+ if (contributors.length === 0)
1212
+ return;
1213
+ console.log(`${dim(` ${title}:`)} ${hint}`);
1214
+ for (const contributor of contributors) {
1215
+ console.log(`${dim(" -")} ${formatBytes(contributor.size).padStart(9)} ${contributor.path} ${dim(`(${contributor.files} ${contributor.files === 1 ? "file" : "files"})`)}`);
1216
+ }
1217
+ }
1092
1218
  function printHeading(name) {
1093
1219
  console.log(`${bold(name)} ${dim(`${symbol("→", "-")} Freestyle sync`)}`);
1094
1220
  console.log("");
@@ -1666,13 +1792,20 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1666
1792
  const canRenderInlineProgress = process.stdout.isTTY;
1667
1793
  const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
1668
1794
  const chunkManifest = [];
1669
- let index = 0;
1670
- for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
1671
- const chunkName = `${String(index).padStart(width, "0")}.chunk`;
1672
- const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1673
- chunkManifest.push(`${chunkName} ${content.length}`);
1674
- await vm.fs.writeFile(`${chunkDir}/${chunkName}`, content);
1675
- 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
+ }
1676
1809
  if (chunkCount > 1) {
1677
1810
  const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
1678
1811
  if (canRenderInlineProgress) {
@@ -1685,7 +1818,38 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1685
1818
  console.log(progressMessage);
1686
1819
  }
1687
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);
1688
1846
  index += 1;
1847
+ while (inFlightUploads.size >= targetUploadConcurrency) {
1848
+ await waitForUploadedChunk();
1849
+ }
1850
+ }
1851
+ while (inFlightUploads.size > 0) {
1852
+ await waitForUploadedChunk();
1689
1853
  }
1690
1854
  if (archiveSize > 0) {
1691
1855
  await vm.fs.writeTextFile(`${chunkDir}/manifest`, `${chunkManifest.join("\n")}\n`);
@@ -1756,6 +1920,21 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
1756
1920
  `rm -rf ${shellQuote(chunkDir)}`,
1757
1921
  ].join("\n"), 5 * 60 * MS_PER_SECOND);
1758
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
+ }
1759
1938
  async function mkdirRemote(vm, directories) {
1760
1939
  for (const chunk of chunkArray(directories, 50)) {
1761
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.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "main": "dist/src/main.js",
6
6
  "exports": {