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.
- package/dist/src/main.js +191 -12
- 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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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(" ")}`);
|