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.
- package/dist/src/main.js +152 -10
- 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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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(" ")}`);
|