freestyle-sync 0.1.10 → 0.1.12
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 +49 -42
- package/dist/src/main.d.ts +46 -0
- package/dist/src/main.js +113 -8
- package/dist/src/plugin-api.d.ts +127 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
# Freestyle Sync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Offload any development environment to a VM with 1 command. Authentication, agent context, and dependencies are automatically handled.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx freestyle-sync
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
npx freestyle-sync --provider apple-container --apple-container-image ubuntu:24.04
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Use `--vm-id <container-name-or-id>` to reuse an existing Apple container. Snapshot caching is only available with the Freestyle provider.
|
|
16
|
-
|
|
17
|
-
Use the SDK when embedding sync inside another CLI/app (no required `freestyle-sync.config.ts` or cache file):
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import { defineConfig, sync, type SyncCache } from "freestyle-sync";
|
|
21
|
-
import { gitAuthPlugin } from "@freestyle-sync/auth-git";
|
|
22
|
-
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
23
|
-
|
|
24
|
-
let cache: SyncCache | undefined;
|
|
25
|
-
|
|
26
|
-
const result = await sync({
|
|
27
|
-
config: defineConfig({
|
|
28
|
-
plugins: [gitAuthPlugin(), nodeNpmPlugin()],
|
|
29
|
-
}),
|
|
30
|
-
options: {
|
|
31
|
-
projectRoot: process.cwd(),
|
|
32
|
-
autoSsh: false,
|
|
33
|
-
},
|
|
34
|
-
cache,
|
|
35
|
-
onCacheUpdate(nextCache) {
|
|
36
|
-
cache = nextCache;
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
console.log("synced runtime", result.vmId);
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
freestyle-sync creates `freestyle-sync.config.mjs` on first run and installs the config dependencies into your project as dev dependencies. Remove the generated config or dependencies if you want to reset it later.
|
|
9
|
+
On it's first run, freestyle-sync creates `freestyle-sync.config.mjs` and configures a large set of default plugins. Plugins help sync parts of your development environment that require more complex logic than just copying the current directory's files.
|
|
44
10
|
|
|
45
11
|
```js
|
|
46
12
|
import { defineConfig } from "freestyle-sync";
|
|
@@ -57,8 +23,11 @@ import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
|
|
|
57
23
|
import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
|
|
58
24
|
import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
|
|
59
25
|
import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
|
|
26
|
+
import { nextjsPlugin } from "@freestyle-sync/nextjs";
|
|
60
27
|
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
28
|
+
import { rustPlugin } from "@freestyle-sync/rust";
|
|
61
29
|
import { shellHistoryPlugin } from "@freestyle-sync/shell-history";
|
|
30
|
+
import { vitePlugin } from "@freestyle-sync/vite";
|
|
62
31
|
|
|
63
32
|
export default defineConfig({
|
|
64
33
|
plugins: [
|
|
@@ -73,6 +42,9 @@ export default defineConfig({
|
|
|
73
42
|
azureAuthPlugin(),
|
|
74
43
|
gcloudAuthPlugin(),
|
|
75
44
|
nodeNpmPlugin(),
|
|
45
|
+
nextjsPlugin(),
|
|
46
|
+
vitePlugin(),
|
|
47
|
+
rustPlugin(),
|
|
76
48
|
claudeAgentPlugin(),
|
|
77
49
|
codexAgentPlugin(),
|
|
78
50
|
copilotAgentPlugin(),
|
|
@@ -81,7 +53,7 @@ export default defineConfig({
|
|
|
81
53
|
});
|
|
82
54
|
```
|
|
83
55
|
|
|
84
|
-
|
|
56
|
+
You 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
57
|
|
|
86
58
|
```js
|
|
87
59
|
export default defineConfig({
|
|
@@ -90,15 +62,50 @@ export default defineConfig({
|
|
|
90
62
|
include: [
|
|
91
63
|
{ source: "../shared-config", target: "shared-config" },
|
|
92
64
|
],
|
|
93
|
-
}
|
|
94
|
-
plugins: [
|
|
95
|
-
nodeNpmPlugin(),
|
|
96
|
-
],
|
|
65
|
+
}
|
|
97
66
|
});
|
|
98
67
|
```
|
|
99
68
|
|
|
100
|
-
|
|
69
|
+
The default Node/framework/runtime plugins avoid uploading generated dependency and build output folders:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
nodeNpmPlugin() // excludes node_modules and installs frozen dependencies remotely
|
|
73
|
+
nextjsPlugin() // excludes .next
|
|
74
|
+
vitePlugin() // excludes dist, build, and .vite
|
|
75
|
+
rustPlugin() // excludes target, installs Rust/Cargo, and runs cargo fetch
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
You can keep uploading those generated folders when needed:
|
|
101
79
|
|
|
102
80
|
```js
|
|
103
81
|
nodeNpmPlugin({ syncNodeModules: true })
|
|
82
|
+
nextjsPlugin({ syncNextDir: true })
|
|
83
|
+
vitePlugin({ syncBuildOutput: true })
|
|
84
|
+
rustPlugin({ syncTargetDir: true })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If you're integrating freestyle-sync into your own toolchain, you can use it as an sdk and take full control over how configuration and cache is stored.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { defineConfig, sync, type SyncCache } from "freestyle-sync";
|
|
91
|
+
import { gitAuthPlugin } from "@freestyle-sync/auth-git";
|
|
92
|
+
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
93
|
+
|
|
94
|
+
let cache: SyncCache | undefined;
|
|
95
|
+
|
|
96
|
+
const result = await sync({
|
|
97
|
+
config: defineConfig({
|
|
98
|
+
plugins: [gitAuthPlugin(), nodeNpmPlugin()],
|
|
99
|
+
}),
|
|
100
|
+
options: {
|
|
101
|
+
projectRoot: process.cwd(),
|
|
102
|
+
autoSsh: false,
|
|
103
|
+
},
|
|
104
|
+
cache,
|
|
105
|
+
onCacheUpdate(nextCache) {
|
|
106
|
+
cache = nextCache;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log("synced runtime", result.vmId);
|
|
104
111
|
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import type { CliOptions, PushvmConfig, VmProviderName } from "./plugin-api.ts";
|
|
4
|
+
export * from "./plugin-api.ts";
|
|
5
|
+
type FileKind = "file" | "symlink";
|
|
6
|
+
type FileDigest = {
|
|
7
|
+
hash: string;
|
|
8
|
+
kind: FileKind;
|
|
9
|
+
mode: number;
|
|
10
|
+
size: number;
|
|
11
|
+
};
|
|
12
|
+
type CacheFile = {
|
|
13
|
+
version: number;
|
|
14
|
+
projectRoot: string;
|
|
15
|
+
remoteProjectDir: string;
|
|
16
|
+
provider?: VmProviderName;
|
|
17
|
+
vmId?: string;
|
|
18
|
+
snapshotId?: string;
|
|
19
|
+
projectFiles: Record<string, FileDigest>;
|
|
20
|
+
contextFiles: Record<string, FileDigest>;
|
|
21
|
+
snapshotProjectFiles?: Record<string, FileDigest>;
|
|
22
|
+
snapshotContextFiles?: Record<string, FileDigest>;
|
|
23
|
+
envHash?: string;
|
|
24
|
+
snapshotEnvHash?: string;
|
|
25
|
+
updatedAt?: string;
|
|
26
|
+
};
|
|
27
|
+
type PluginPreferences = {
|
|
28
|
+
version: number;
|
|
29
|
+
disabledPlugins: string[];
|
|
30
|
+
updatedAt?: string;
|
|
31
|
+
};
|
|
32
|
+
export type SyncCache = CacheFile;
|
|
33
|
+
export type SyncPluginPreferences = PluginPreferences;
|
|
34
|
+
export type SyncSdkOptions = {
|
|
35
|
+
config: PushvmConfig;
|
|
36
|
+
options?: Partial<CliOptions>;
|
|
37
|
+
cache?: SyncCache;
|
|
38
|
+
pluginPreferences?: SyncPluginPreferences;
|
|
39
|
+
onCacheUpdate?(cache: SyncCache): Promise<void> | void;
|
|
40
|
+
};
|
|
41
|
+
export type SyncSdkResult = {
|
|
42
|
+
vmId?: string;
|
|
43
|
+
cache: SyncCache;
|
|
44
|
+
snapshotId?: string;
|
|
45
|
+
};
|
|
46
|
+
export declare function sync(sdkOptions: SyncSdkOptions): Promise<SyncSdkResult>;
|
package/dist/src/main.js
CHANGED
|
@@ -30,6 +30,8 @@ const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
|
|
|
30
30
|
const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
|
|
31
31
|
const DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY = "pck_h2aft7g9pqjzrkdnzs199h1may5wjtdtdxeex7m2wzp1r";
|
|
32
32
|
const STACK_REFRESH_TOKEN_ENV_KEY = "FREESTYLE_STACK_REFRESH_TOKEN";
|
|
33
|
+
const GENERATED_UPLOAD_DIRECTORY_NAMES = new Set([".cache", ".freestyle-sync", ".git", ".next", ".turbo", ".vercel", ".vite", "build", "coverage", "dist", "node_modules", "target"]);
|
|
34
|
+
const DEFAULT_PROJECT_EXCLUDES = [".freestyle-sync"];
|
|
33
35
|
const USE_UNICODE_OUTPUT = process.stdout.isTTY && (process.env.TERM !== "dumb" || Boolean(process.env.TERM_PROGRAM));
|
|
34
36
|
const USE_STYLED_OUTPUT = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
35
37
|
const DEFAULT_CONFIG_DEPENDENCIES = [
|
|
@@ -47,8 +49,11 @@ const DEFAULT_CONFIG_DEPENDENCIES = [
|
|
|
47
49
|
{ name: "@freestyle-sync/auth-npm", spec: "@freestyle-sync/auth-npm@latest" },
|
|
48
50
|
{ name: "@freestyle-sync/auth-ssh", spec: "@freestyle-sync/auth-ssh@latest" },
|
|
49
51
|
{ name: "@freestyle-sync/auth-yarn", spec: "@freestyle-sync/auth-yarn@latest" },
|
|
52
|
+
{ name: "@freestyle-sync/nextjs", spec: "@freestyle-sync/nextjs@latest" },
|
|
50
53
|
{ name: "@freestyle-sync/node-npm", spec: "@freestyle-sync/node-npm@latest" },
|
|
54
|
+
{ name: "@freestyle-sync/rust", spec: "@freestyle-sync/rust@latest" },
|
|
51
55
|
{ name: "@freestyle-sync/shell-history", spec: "@freestyle-sync/shell-history@latest" },
|
|
56
|
+
{ name: "@freestyle-sync/vite", spec: "@freestyle-sync/vite@latest" },
|
|
52
57
|
];
|
|
53
58
|
const pluginUtils = {
|
|
54
59
|
checkedExec,
|
|
@@ -107,7 +112,6 @@ async function main() {
|
|
|
107
112
|
}
|
|
108
113
|
export async function sync(sdkOptions) {
|
|
109
114
|
const options = await resolveCliOptions(sdkOptions.options);
|
|
110
|
-
printHeading(CLI_NAME);
|
|
111
115
|
config = sdkOptions.config;
|
|
112
116
|
plugins = config.plugins;
|
|
113
117
|
const pluginPreferences = await updatePluginPreferences(options, sdkOptions.pluginPreferences);
|
|
@@ -375,8 +379,11 @@ import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
|
|
|
375
379
|
import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
|
|
376
380
|
import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
|
|
377
381
|
import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
|
|
382
|
+
import { nextjsPlugin } from "@freestyle-sync/nextjs";
|
|
378
383
|
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
384
|
+
import { rustPlugin } from "@freestyle-sync/rust";
|
|
379
385
|
import { shellHistoryPlugin } from "@freestyle-sync/shell-history";
|
|
386
|
+
import { vitePlugin } from "@freestyle-sync/vite";
|
|
380
387
|
import { defineConfig } from "freestyle-sync";
|
|
381
388
|
|
|
382
389
|
export default defineConfig({
|
|
@@ -392,6 +399,9 @@ export default defineConfig({
|
|
|
392
399
|
azureAuthPlugin(),
|
|
393
400
|
gcloudAuthPlugin(),
|
|
394
401
|
nodeNpmPlugin(),
|
|
402
|
+
nextjsPlugin(),
|
|
403
|
+
vitePlugin(),
|
|
404
|
+
rustPlugin(),
|
|
395
405
|
claudeAgentPlugin(),
|
|
396
406
|
codexAgentPlugin(),
|
|
397
407
|
copilotAgentPlugin(),
|
|
@@ -414,8 +424,11 @@ import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
|
|
|
414
424
|
import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
|
|
415
425
|
import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
|
|
416
426
|
import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
|
|
427
|
+
import { nextjsPlugin } from "@freestyle-sync/nextjs";
|
|
417
428
|
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
429
|
+
import { rustPlugin } from "@freestyle-sync/rust";
|
|
418
430
|
import { shellHistoryPlugin } from "@freestyle-sync/shell-history";
|
|
431
|
+
import { vitePlugin } from "@freestyle-sync/vite";
|
|
419
432
|
import { defineConfig } from "freestyle-sync";
|
|
420
433
|
|
|
421
434
|
export default defineConfig({
|
|
@@ -431,6 +444,9 @@ export default defineConfig({
|
|
|
431
444
|
azureAuthPlugin(),
|
|
432
445
|
gcloudAuthPlugin(),
|
|
433
446
|
nodeNpmPlugin(),
|
|
447
|
+
nextjsPlugin(),
|
|
448
|
+
vitePlugin(),
|
|
449
|
+
rustPlugin(),
|
|
434
450
|
claudeAgentPlugin(),
|
|
435
451
|
codexAgentPlugin(),
|
|
436
452
|
copilotAgentPlugin(),
|
|
@@ -730,7 +746,7 @@ async function resolveProjectSyncConfig(options) {
|
|
|
730
746
|
configs.push(pluginConfig);
|
|
731
747
|
}
|
|
732
748
|
return {
|
|
733
|
-
exclude: configs.flatMap((projectConfig) => projectConfig.exclude ?? []).map(normalizeProjectPattern),
|
|
749
|
+
exclude: [...DEFAULT_PROJECT_EXCLUDES, ...configs.flatMap((projectConfig) => projectConfig.exclude ?? [])].map(normalizeProjectPattern),
|
|
734
750
|
include: configs.flatMap((projectConfig) => normalizeProjectIncludes(options.projectRoot, projectConfig.include ?? [])),
|
|
735
751
|
};
|
|
736
752
|
}
|
|
@@ -1066,6 +1082,8 @@ function printPlan(options, projectChanges, contextChanges, envExports, cache) {
|
|
|
1066
1082
|
console.log(`${dim(" Project files:")} ${projectChanges.changed.length} changed, ${projectChanges.removed.length} removed, ${projectChanges.unchanged} unchanged`);
|
|
1067
1083
|
console.log(`${dim(" Context files:")} ${contextChanges.changed.length} changed, ${contextChanges.removed.length} removed, ${contextChanges.unchanged} unchanged`);
|
|
1068
1084
|
console.log(`${dim(" Estimated upload:")} ${formatBytes(totalEntrySize(projectChanges.changed))} project, ${formatBytes(totalEntrySize(contextChanges.changed))} context`);
|
|
1085
|
+
printUploadBreakdown("Project upload contributors", projectUploadContributors(projectChanges.changed), "consider sync.exclude for generated folders");
|
|
1086
|
+
printUploadBreakdown("Context upload contributors", contextUploadContributors(contextChanges.changed), "disable unneeded context plugins to skip");
|
|
1069
1087
|
if (Object.keys(envExports).length > 0) {
|
|
1070
1088
|
console.log(`${dim(" Environment exports:")} ${Object.keys(envExports).length}`);
|
|
1071
1089
|
}
|
|
@@ -1074,6 +1092,40 @@ function printPlan(options, projectChanges, contextChanges, envExports, cache) {
|
|
|
1074
1092
|
function totalEntrySize(entries) {
|
|
1075
1093
|
return entries.reduce((total, entry) => total + entry.size, 0);
|
|
1076
1094
|
}
|
|
1095
|
+
function projectUploadContributors(entries) {
|
|
1096
|
+
return largestContributors(entries, (entry) => contributionPath(entry.relativePath));
|
|
1097
|
+
}
|
|
1098
|
+
function contextUploadContributors(entries) {
|
|
1099
|
+
return largestContributors(entries, (entry) => entry.label);
|
|
1100
|
+
}
|
|
1101
|
+
function contributionPath(relativePath) {
|
|
1102
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
1103
|
+
if (parts.length <= 1)
|
|
1104
|
+
return relativePath;
|
|
1105
|
+
const generatedDirectoryIndex = parts.findIndex((part) => GENERATED_UPLOAD_DIRECTORY_NAMES.has(part));
|
|
1106
|
+
if (generatedDirectoryIndex >= 0)
|
|
1107
|
+
return parts.slice(0, generatedDirectoryIndex + 1).join("/");
|
|
1108
|
+
return parts.slice(0, Math.min(parts.length - 1, 3)).join("/");
|
|
1109
|
+
}
|
|
1110
|
+
function largestContributors(entries, bucketForEntry) {
|
|
1111
|
+
const buckets = new Map();
|
|
1112
|
+
for (const entry of entries) {
|
|
1113
|
+
const bucketName = bucketForEntry(entry);
|
|
1114
|
+
const bucket = buckets.get(bucketName) ?? { path: bucketName, size: 0, files: 0 };
|
|
1115
|
+
bucket.size += entry.size;
|
|
1116
|
+
bucket.files += 1;
|
|
1117
|
+
buckets.set(bucketName, bucket);
|
|
1118
|
+
}
|
|
1119
|
+
return Array.from(buckets.values()).sort((left, right) => right.size - left.size).slice(0, 6);
|
|
1120
|
+
}
|
|
1121
|
+
function printUploadBreakdown(title, contributors, hint) {
|
|
1122
|
+
if (contributors.length === 0)
|
|
1123
|
+
return;
|
|
1124
|
+
console.log(`${dim(` ${title}:`)} ${hint}`);
|
|
1125
|
+
for (const contributor of contributors) {
|
|
1126
|
+
console.log(`${dim(" -")} ${formatBytes(contributor.size).padStart(9)} ${contributor.path} ${dim(`(${contributor.files} ${contributor.files === 1 ? "file" : "files"})`)}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1077
1129
|
function printHeading(name) {
|
|
1078
1130
|
console.log(`${bold(name)} ${dim(`${symbol("→", "-")} Freestyle sync`)}`);
|
|
1079
1131
|
console.log("");
|
|
@@ -1581,6 +1633,8 @@ async function runInstall(vm, projectRoot, remoteProjectDir) {
|
|
|
1581
1633
|
await checkedExec(vm, `cd ${shellQuote(remoteProjectDir)} && ${installCommand}`, 20 * 60 * 1000);
|
|
1582
1634
|
}
|
|
1583
1635
|
async function detectInstallCommand(projectRoot) {
|
|
1636
|
+
if (await exists(path.join(projectRoot, "bun.lock")) || await exists(path.join(projectRoot, "bun.lockb")))
|
|
1637
|
+
return "bun install --frozen-lockfile";
|
|
1584
1638
|
if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
|
|
1585
1639
|
return "corepack enable && pnpm install --frozen-lockfile";
|
|
1586
1640
|
if (await exists(path.join(projectRoot, "yarn.lock")))
|
|
@@ -1648,11 +1702,13 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
1648
1702
|
const width = String(chunkCount - 1).length;
|
|
1649
1703
|
const canRenderInlineProgress = process.stdout.isTTY;
|
|
1650
1704
|
const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
|
|
1705
|
+
const chunkManifest = [];
|
|
1651
1706
|
let index = 0;
|
|
1652
1707
|
for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
|
|
1653
|
-
const chunkName = `${String(index).padStart(width, "0")}.chunk
|
|
1708
|
+
const chunkName = `${String(index).padStart(width, "0")}.chunk`;
|
|
1654
1709
|
const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1655
|
-
|
|
1710
|
+
chunkManifest.push(`${chunkName} ${content.length}`);
|
|
1711
|
+
await vm.fs.writeFile(`${chunkDir}/${chunkName}`, content);
|
|
1656
1712
|
const uploadedChunks = index + 1;
|
|
1657
1713
|
if (chunkCount > 1) {
|
|
1658
1714
|
const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
|
|
@@ -1668,14 +1724,63 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
1668
1724
|
}
|
|
1669
1725
|
index += 1;
|
|
1670
1726
|
}
|
|
1727
|
+
if (archiveSize > 0) {
|
|
1728
|
+
await vm.fs.writeTextFile(`${chunkDir}/manifest`, `${chunkManifest.join("\n")}\n`);
|
|
1729
|
+
}
|
|
1671
1730
|
await checkedExec(vm, archiveSize === 0
|
|
1672
1731
|
? `: > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`
|
|
1673
1732
|
: [
|
|
1674
1733
|
"set -e",
|
|
1675
|
-
`
|
|
1676
|
-
`
|
|
1677
|
-
`
|
|
1734
|
+
`chunk_dir=${shellQuote(chunkDir)}`,
|
|
1735
|
+
`remote_archive=${shellQuote(remoteArchivePath)}`,
|
|
1736
|
+
`manifest="$chunk_dir/manifest"`,
|
|
1737
|
+
"deadline=$(( $(date +%s) + 60 ))",
|
|
1738
|
+
"while :; do",
|
|
1739
|
+
" validation_error=",
|
|
1740
|
+
" validated_chunks=0",
|
|
1741
|
+
" if [ ! -f \"$manifest\" ]; then",
|
|
1742
|
+
" validation_error='missing chunk manifest'",
|
|
1743
|
+
" else",
|
|
1744
|
+
" actual_chunks=$(find \"$chunk_dir\" -maxdepth 1 -type f -name '*.chunk' | wc -l | tr -d '[:space:]')",
|
|
1745
|
+
` if [ "$actual_chunks" != ${shellQuote(String(chunkCount))} ]; then`,
|
|
1746
|
+
` validation_error="expected ${chunkCount} chunks, found $actual_chunks"`,
|
|
1747
|
+
" else",
|
|
1748
|
+
" while IFS=' ' read -r chunk_name expected_size; do",
|
|
1749
|
+
" [ -n \"$chunk_name\" ] || continue",
|
|
1750
|
+
" chunk_path=\"$chunk_dir/$chunk_name\"",
|
|
1751
|
+
" if [ ! -f \"$chunk_path\" ]; then",
|
|
1752
|
+
" validation_error=\"missing chunk $chunk_name\"",
|
|
1753
|
+
" break",
|
|
1754
|
+
" fi",
|
|
1755
|
+
" actual_chunk_size=$(wc -c < \"$chunk_path\" | tr -d '[:space:]')",
|
|
1756
|
+
" if [ \"$actual_chunk_size\" != \"$expected_size\" ]; then",
|
|
1757
|
+
" validation_error=\"chunk $chunk_name expected $expected_size bytes, got $actual_chunk_size\"",
|
|
1758
|
+
" break",
|
|
1759
|
+
" fi",
|
|
1760
|
+
" validated_chunks=$((validated_chunks + 1))",
|
|
1761
|
+
" done < \"$manifest\"",
|
|
1762
|
+
` if [ "$validated_chunks" != ${shellQuote(String(chunkCount))} ] && [ -z "$validation_error" ]; then`,
|
|
1763
|
+
` validation_error="expected ${chunkCount} manifest entries, found $validated_chunks"`,
|
|
1764
|
+
" fi",
|
|
1765
|
+
" fi",
|
|
1766
|
+
" fi",
|
|
1767
|
+
" if [ -z \"$validation_error\" ]; then",
|
|
1768
|
+
" break",
|
|
1769
|
+
" fi",
|
|
1770
|
+
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then",
|
|
1771
|
+
` rm -f ${shellQuote(remoteArchivePath)}`,
|
|
1772
|
+
` rm -rf ${shellQuote(chunkDir)}`,
|
|
1773
|
+
` echo ${shellQuote(`archive chunk validation failed for ${label}`)} >&2`,
|
|
1774
|
+
" echo \"$validation_error\" >&2",
|
|
1775
|
+
" exit 1",
|
|
1776
|
+
" fi",
|
|
1777
|
+
" sleep 1",
|
|
1678
1778
|
"done",
|
|
1779
|
+
`rm -f ${shellQuote(remoteArchivePath)}`,
|
|
1780
|
+
"while IFS=' ' read -r chunk_name expected_size; do",
|
|
1781
|
+
" [ -n \"$chunk_name\" ] || continue",
|
|
1782
|
+
" cat \"$chunk_dir/$chunk_name\" >> \"$remote_archive\"",
|
|
1783
|
+
"done < \"$manifest\"",
|
|
1679
1784
|
`actual_size=$(wc -c < ${shellQuote(remoteArchivePath)} | tr -d '[:space:]')`,
|
|
1680
1785
|
`actual_hash=$(sha256sum ${shellQuote(remoteArchivePath)} | awk '{print $1}')`,
|
|
1681
1786
|
`if [ "$actual_size" != ${shellQuote(String(archiveSize))} ] || [ "$actual_hash" != ${shellQuote(archiveHash)} ]; then`,
|
|
@@ -1686,7 +1791,7 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
1686
1791
|
" exit 1",
|
|
1687
1792
|
"fi",
|
|
1688
1793
|
`rm -rf ${shellQuote(chunkDir)}`,
|
|
1689
|
-
].join("\n"));
|
|
1794
|
+
].join("\n"), 5 * 60 * MS_PER_SECOND);
|
|
1690
1795
|
}
|
|
1691
1796
|
async function mkdirRemote(vm, directories) {
|
|
1692
1797
|
for (const chunk of chunkArray(directories, 50)) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export type VmProviderName = "freestyle" | "apple-container";
|
|
2
|
+
export type CliOptions = {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
cachePath: string;
|
|
5
|
+
remoteProjectDir: string;
|
|
6
|
+
provider: VmProviderName;
|
|
7
|
+
name: string;
|
|
8
|
+
vmId?: string;
|
|
9
|
+
appleContainerImage: string;
|
|
10
|
+
idleTimeoutSeconds?: number;
|
|
11
|
+
yes: boolean;
|
|
12
|
+
dryRun: boolean;
|
|
13
|
+
disablePlugins: string[];
|
|
14
|
+
enablePlugins: string[];
|
|
15
|
+
resetPluginPrefs: boolean;
|
|
16
|
+
listPlugins: boolean;
|
|
17
|
+
includeAuth: boolean;
|
|
18
|
+
includeAgentContext: boolean;
|
|
19
|
+
includeGitDir: boolean;
|
|
20
|
+
includeAllCopilotWorkspaces: boolean;
|
|
21
|
+
snapshot: boolean;
|
|
22
|
+
skipSync: boolean;
|
|
23
|
+
install: boolean;
|
|
24
|
+
autoSsh: boolean;
|
|
25
|
+
envKeys: string[];
|
|
26
|
+
};
|
|
27
|
+
export type ContextCandidate = {
|
|
28
|
+
source: string;
|
|
29
|
+
remoteRoot: string;
|
|
30
|
+
label: string;
|
|
31
|
+
sensitive: boolean;
|
|
32
|
+
preferenceKey: string;
|
|
33
|
+
promptLabel: string;
|
|
34
|
+
};
|
|
35
|
+
export type RemoteVm = {
|
|
36
|
+
exec(args: {
|
|
37
|
+
command: string;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
stdout?: string | null;
|
|
41
|
+
stderr?: string | null;
|
|
42
|
+
statusCode?: number | null;
|
|
43
|
+
}>;
|
|
44
|
+
fs: {
|
|
45
|
+
writeFile(path: string, content: Buffer): Promise<void>;
|
|
46
|
+
writeTextFile(path: string, content: string): Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export type UploadArchiveInChunks = (vm: RemoteVm, vmId: string, archivePath: string, remoteArchivePath: string, label: string) => Promise<void>;
|
|
50
|
+
export type ExecFileAsync = (file: string, args: string[]) => Promise<{
|
|
51
|
+
stdout: string;
|
|
52
|
+
stderr: string;
|
|
53
|
+
}>;
|
|
54
|
+
export type PushvmPluginUtils = {
|
|
55
|
+
checkedExec(vm: RemoteVm, command: string, timeoutMs?: number): Promise<{
|
|
56
|
+
stdout?: string | null;
|
|
57
|
+
stderr?: string | null;
|
|
58
|
+
statusCode?: number | null;
|
|
59
|
+
}>;
|
|
60
|
+
createTar(args: string[]): Promise<void>;
|
|
61
|
+
uploadArchiveInChunks: UploadArchiveInChunks;
|
|
62
|
+
execFileAsync: ExecFileAsync;
|
|
63
|
+
shellQuote(value: string): string;
|
|
64
|
+
md5(value: string): string;
|
|
65
|
+
delay(ms: number): Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
export type PushvmPluginContext = {
|
|
68
|
+
options: CliOptions;
|
|
69
|
+
utils: PushvmPluginUtils;
|
|
70
|
+
};
|
|
71
|
+
export type RemoteHookContext = PushvmPluginContext & {
|
|
72
|
+
vm: RemoteVm;
|
|
73
|
+
vmId: string;
|
|
74
|
+
};
|
|
75
|
+
export type EditorScheme = string;
|
|
76
|
+
export type EditorHookContext = RemoteHookContext & {
|
|
77
|
+
scheme: EditorScheme;
|
|
78
|
+
remoteWorkspaceUri: string;
|
|
79
|
+
};
|
|
80
|
+
export type ConnectHookContext = RemoteHookContext & {
|
|
81
|
+
contextCandidates: ContextCandidate[];
|
|
82
|
+
runBeforeOpenRemoteEditor(args: {
|
|
83
|
+
scheme: EditorScheme;
|
|
84
|
+
remoteWorkspaceUri: string;
|
|
85
|
+
}): Promise<string[]>;
|
|
86
|
+
runAfterOpenRemoteEditor(args: {
|
|
87
|
+
scheme: EditorScheme;
|
|
88
|
+
remoteWorkspaceUri: string;
|
|
89
|
+
}): Promise<string[]>;
|
|
90
|
+
};
|
|
91
|
+
export type ProjectSyncInclude = string | {
|
|
92
|
+
source: string;
|
|
93
|
+
target?: string;
|
|
94
|
+
};
|
|
95
|
+
export type ProjectSyncConfig = {
|
|
96
|
+
exclude?: string[];
|
|
97
|
+
include?: ProjectSyncInclude[];
|
|
98
|
+
};
|
|
99
|
+
export type PushvmPlugin = {
|
|
100
|
+
name: string;
|
|
101
|
+
collectEnvironment?(context: PushvmPluginContext): Record<string, string>;
|
|
102
|
+
discoverContextCandidates?(context: PushvmPluginContext): Promise<ContextCandidate[]> | ContextCandidate[];
|
|
103
|
+
configureProjectSync?(context: PushvmPluginContext): Promise<ProjectSyncConfig | void> | ProjectSyncConfig | void;
|
|
104
|
+
shouldSkipContextDirectory?(relativePath: string, name: string): boolean;
|
|
105
|
+
isProtectedRemotePath?(remotePath: string): boolean;
|
|
106
|
+
afterProjectSync?(context: RemoteHookContext): Promise<void> | void;
|
|
107
|
+
afterContextSync?(context: RemoteHookContext & {
|
|
108
|
+
changedRemotePaths: string[];
|
|
109
|
+
}): Promise<void> | void;
|
|
110
|
+
beforeSnapshot?(context: RemoteHookContext): Promise<void> | void;
|
|
111
|
+
afterSync?(context: RemoteHookContext & {
|
|
112
|
+
contextCandidates: ContextCandidate[];
|
|
113
|
+
}): Promise<string[] | void> | string[] | void;
|
|
114
|
+
connect?(context: ConnectHookContext): Promise<boolean | void> | boolean | void;
|
|
115
|
+
beforeOpenRemoteEditor?(context: EditorHookContext & {
|
|
116
|
+
contextCandidates: ContextCandidate[];
|
|
117
|
+
}): Promise<string[] | void> | string[] | void;
|
|
118
|
+
afterOpenRemoteEditor?(context: EditorHookContext & {
|
|
119
|
+
contextCandidates: ContextCandidate[];
|
|
120
|
+
}): Promise<string[] | void> | string[] | void;
|
|
121
|
+
};
|
|
122
|
+
export type PushvmConfig = {
|
|
123
|
+
plugins: PushvmPlugin[];
|
|
124
|
+
sync?: ProjectSyncConfig;
|
|
125
|
+
};
|
|
126
|
+
export declare function definePlugin(plugin: PushvmPlugin): PushvmPlugin;
|
|
127
|
+
export declare function defineConfig(config: PushvmConfig): PushvmConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freestyle-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/main.js",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"publish": "npm run check && npm run build && npm run publish:workspaces && npm run publish:root",
|
|
26
26
|
"publish:root": "node scripts/publish-root.mjs",
|
|
27
27
|
"publish:workspaces": "node scripts/publish-workspaces.mjs",
|
|
28
|
-
"start": "node src/main.ts"
|
|
28
|
+
"start": "node src/main.ts",
|
|
29
|
+
"version:patch": "node scripts/version-workspaces.mjs patch",
|
|
30
|
+
"version:workspaces": "node scripts/version-workspaces.mjs"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"dotenv": "^17.4.2",
|