freestyle-sync 0.1.9 → 0.1.10

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
@@ -79,4 +79,26 @@ export default defineConfig({
79
79
  shellHistoryPlugin(),
80
80
  ],
81
81
  });
82
- ```
82
+ ```
83
+
84
+ Project sync can exclude folders and include additional local paths. Include paths may point outside the project directory; they are copied into the remote project at the configured relative target.
85
+
86
+ ```js
87
+ export default defineConfig({
88
+ sync: {
89
+ exclude: ["dist", ".turbo"],
90
+ include: [
91
+ { source: "../shared-config", target: "shared-config" },
92
+ ],
93
+ },
94
+ plugins: [
95
+ nodeNpmPlugin(),
96
+ ],
97
+ });
98
+ ```
99
+
100
+ `nodeNpmPlugin()` excludes `node_modules` from upload by default and installs dependencies on the remote runtime using the project lockfile: `pnpm install --frozen-lockfile`, Yarn `--frozen-lockfile`/`--immutable`, or `npm ci`. To keep the previous behavior of syncing `node_modules` and only repairing native/workspace packages remotely, use:
101
+
102
+ ```js
103
+ nodeNpmPlugin({ syncNodeModules: true })
104
+ ```
package/dist/src/main.js CHANGED
@@ -10,7 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  import "dotenv/config";
11
11
  import { createHash } from "node:crypto";
12
12
  import { createReadStream, realpathSync } from "node:fs";
13
- import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
13
+ import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readlink, rm, stat, symlink, writeFile } from "node:fs/promises";
14
14
  import { homedir, tmpdir } from "node:os";
15
15
  import path from "node:path";
16
16
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -182,7 +182,8 @@ export async function sync(sdkOptions) {
182
182
  progress.step("Scanning project files");
183
183
  const cache = currentCache;
184
184
  const base = cacheBaseForSync(options, cache);
185
- const projectEntries = await scanProject(options.projectRoot, options.includeGitDir);
185
+ const projectSyncConfig = await resolveProjectSyncConfig(options);
186
+ const projectEntries = await scanProject(options.projectRoot, options.includeGitDir, projectSyncConfig);
186
187
  const projectCurrent = digestMap(projectEntries);
187
188
  const projectChanges = diffEntries(projectEntries, base.projectFiles);
188
189
  progress.step("Detecting auth and agent context");
@@ -719,23 +720,92 @@ async function writeCache(cachePath, cache) {
719
720
  await mkdir(path.dirname(cachePath), { recursive: true });
720
721
  await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
721
722
  }
722
- async function scanProject(projectRoot, includeGitDir) {
723
- const entries = [];
724
- await walk(projectRoot, "", entries, {
723
+ async function resolveProjectSyncConfig(options) {
724
+ const configs = [];
725
+ if (config.sync)
726
+ configs.push(config.sync);
727
+ for (const plugin of plugins) {
728
+ const pluginConfig = await plugin.configureProjectSync?.({ options, utils: pluginUtils });
729
+ if (pluginConfig)
730
+ configs.push(pluginConfig);
731
+ }
732
+ return {
733
+ exclude: configs.flatMap((projectConfig) => projectConfig.exclude ?? []).map(normalizeProjectPattern),
734
+ include: configs.flatMap((projectConfig) => normalizeProjectIncludes(options.projectRoot, projectConfig.include ?? [])),
735
+ };
736
+ }
737
+ function normalizeProjectIncludes(projectRoot, includes) {
738
+ return includes.map((include) => {
739
+ const source = typeof include === "string" ? include : include.source;
740
+ const target = typeof include === "string" ? undefined : include.target;
741
+ const resolvedSource = path.resolve(projectRoot, source);
742
+ return {
743
+ source: resolvedSource,
744
+ target: normalizeProjectPattern(target ?? path.basename(resolvedSource)),
745
+ };
746
+ });
747
+ }
748
+ function normalizeProjectPattern(value) {
749
+ const normalized = path.posix.normalize(value.replace(/\\/g, "/")).replace(/^\/+/, "").replace(/\/+$/, "");
750
+ if (!normalized || normalized === ".") {
751
+ throw new Error("project sync paths must not be empty");
752
+ }
753
+ if (normalized === ".." || normalized.startsWith("../")) {
754
+ throw new Error(`project sync paths must stay inside the remote project: ${value}`);
755
+ }
756
+ return normalized;
757
+ }
758
+ async function scanProject(projectRoot, includeGitDir, syncConfig) {
759
+ const entriesByPath = new Map();
760
+ const rootEntries = [];
761
+ await walk(projectRoot, "", rootEntries, {
725
762
  skipDirectory(relativePath, name) {
726
763
  if (!includeGitDir && relativePath === ".git")
727
764
  return true;
728
- return false;
765
+ return shouldSkipProjectDirectory(relativePath, name, syncConfig.exclude);
766
+ },
767
+ });
768
+ upsertProjectEntries(entriesByPath, rootEntries);
769
+ for (const include of syncConfig.include) {
770
+ upsertProjectEntries(entriesByPath, await scanProjectInclude(include, syncConfig.exclude));
771
+ }
772
+ return Array.from(entriesByPath.values()).sort((left, right) => left.relativePath.localeCompare(right.relativePath));
773
+ }
774
+ async function scanProjectInclude(include, exclude) {
775
+ const stats = await lstat(include.source).catch((error) => {
776
+ throw new Error(`project sync include does not exist: ${include.source} (${error instanceof Error ? error.message : String(error)})`);
777
+ });
778
+ if (stats.isFile() || stats.isSymbolicLink()) {
779
+ return [await digestEntry(include.source, include.target)];
780
+ }
781
+ if (!stats.isDirectory()) {
782
+ return [];
783
+ }
784
+ const entries = [];
785
+ await walk(include.source, "", entries, {
786
+ mapRelativePath(relativePath) {
787
+ return `${include.target}/${relativePath}`;
788
+ },
789
+ skipDirectory(relativePath, name) {
790
+ return shouldSkipProjectDirectory(relativePath, name, exclude);
729
791
  },
730
792
  });
731
- return entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
793
+ return entries;
794
+ }
795
+ function upsertProjectEntries(entriesByPath, entries) {
796
+ for (const entry of entries)
797
+ entriesByPath.set(entry.relativePath, entry);
798
+ }
799
+ function shouldSkipProjectDirectory(relativePath, name, exclude) {
800
+ return exclude.some((excludedPath) => relativePath === excludedPath || name === excludedPath);
732
801
  }
733
802
  async function walk(root, relativePath, entries, options) {
734
803
  const absolutePath = path.join(root, relativePath);
735
804
  const dir = await import("node:fs/promises").then((fs) => fs.readdir(absolutePath, { withFileTypes: true }));
736
805
  for (const dirent of dir) {
737
806
  const childRelativePath = relativePath ? path.join(relativePath, dirent.name) : dirent.name;
738
- const normalizedRelativePath = toPosix(childRelativePath);
807
+ const localRelativePath = toPosix(childRelativePath);
808
+ const normalizedRelativePath = options.mapRelativePath?.(localRelativePath) ?? localRelativePath;
739
809
  const childAbsolutePath = path.join(root, childRelativePath);
740
810
  if (dirent.isDirectory()) {
741
811
  if (options.skipDirectory(normalizedRelativePath, dirent.name)) {
@@ -1387,7 +1457,7 @@ async function ensureRemoteBase(vm, remoteProjectDir) {
1387
1457
  async function syncProject(vm, vmId, options, changes) {
1388
1458
  if (changes.changed.length > 0) {
1389
1459
  console.log(`VM ${vmId}: uploading ${changes.changed.length} changed project files...`);
1390
- const archive = await createProjectArchive(options.projectRoot, changes.changed);
1460
+ const archive = await createProjectArchive(changes.changed);
1391
1461
  try {
1392
1462
  await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-project.tgz", "project");
1393
1463
  await checkedExec(vm, `mkdir -p ${shellQuote(options.remoteProjectDir)} && tar --no-same-owner --no-same-permissions -xzf /tmp/freestyle-sync-project.tgz -C ${shellQuote(options.remoteProjectDir)} && rm -f /tmp/freestyle-sync-project.tgz`);
@@ -1512,11 +1582,11 @@ async function runInstall(vm, projectRoot, remoteProjectDir) {
1512
1582
  }
1513
1583
  async function detectInstallCommand(projectRoot) {
1514
1584
  if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
1515
- return "corepack enable && pnpm install";
1585
+ return "corepack enable && pnpm install --frozen-lockfile";
1516
1586
  if (await exists(path.join(projectRoot, "yarn.lock")))
1517
- return "corepack enable && yarn install";
1518
- if (await exists(path.join(projectRoot, "package-lock.json")))
1519
- return "npm install";
1587
+ return "corepack enable && yarn_version=$(yarn --version) && case \"$yarn_version\" in 1.*) yarn install --frozen-lockfile ;; *) yarn install --immutable ;; esac";
1588
+ if (await exists(path.join(projectRoot, "package-lock.json")) || await exists(path.join(projectRoot, "npm-shrinkwrap.json")))
1589
+ return "npm ci";
1520
1590
  if (await exists(path.join(projectRoot, "requirements.txt")))
1521
1591
  return "python3 -m pip install -r requirements.txt";
1522
1592
  if (await exists(path.join(projectRoot, "pyproject.toml")))
@@ -1527,12 +1597,23 @@ async function detectInstallCommand(projectRoot) {
1527
1597
  return "go mod download";
1528
1598
  return undefined;
1529
1599
  }
1530
- async function createProjectArchive(projectRoot, entries) {
1600
+ async function createProjectArchive(entries) {
1531
1601
  const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-"));
1532
- const listPath = path.join(tempDir, "files.list");
1602
+ const stagingDir = path.join(tempDir, "staging");
1533
1603
  const archivePath = path.join(tempDir, "project.tgz");
1534
- await writeFile(listPath, Buffer.from(entries.map((entry) => entry.relativePath).join("\0") + "\0"));
1535
- await createTar(["--null", "--no-xattrs", "-T", listPath, "-czf", archivePath, "-C", projectRoot]);
1604
+ await mkdir(stagingDir, { recursive: true });
1605
+ for (const entry of entries) {
1606
+ const destination = path.join(stagingDir, ...entry.relativePath.split("/"));
1607
+ await mkdir(path.dirname(destination), { recursive: true });
1608
+ if (entry.kind === "symlink") {
1609
+ await symlink(await readlink(entry.absolutePath), destination);
1610
+ }
1611
+ else {
1612
+ await copyFile(entry.absolutePath, destination);
1613
+ await chmod(destination, entry.mode);
1614
+ }
1615
+ }
1616
+ await createTar(["--no-xattrs", "-czf", archivePath, "-C", stagingDir, "."]);
1536
1617
  return archivePath;
1537
1618
  }
1538
1619
  async function createContextArchive(entries) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freestyle-sync",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "main": "dist/src/main.js",
6
6
  "exports": {