freestyle-sync 0.1.7 → 0.1.9
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 +10 -2
- package/dist/src/main.js +235 -61
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# Freestyle Sync
|
|
2
2
|
|
|
3
|
-
Sync your current directory, it's dependencies, and your agent context into a
|
|
3
|
+
Sync your current directory, it's dependencies, and your agent context into a remote runtime. Freestyle VMs support snapshots so only changes are uploaded on later runs.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx freestyle-sync
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Freestyle VMs are the default provider. You can sync into a local Apple container on macOS with the Apple `container` CLI installed:
|
|
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
|
+
|
|
9
17
|
Use the SDK when embedding sync inside another CLI/app (no required `freestyle-sync.config.ts` or cache file):
|
|
10
18
|
|
|
11
19
|
```ts
|
|
@@ -29,7 +37,7 @@ const result = await sync({
|
|
|
29
37
|
},
|
|
30
38
|
});
|
|
31
39
|
|
|
32
|
-
console.log("synced
|
|
40
|
+
console.log("synced runtime", result.vmId);
|
|
33
41
|
```
|
|
34
42
|
|
|
35
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.
|
package/dist/src/main.js
CHANGED
|
@@ -22,8 +22,9 @@ const execFileAsync = promisify(execFile);
|
|
|
22
22
|
const CLI_NAME = "freestyle-sync";
|
|
23
23
|
const CACHE_VERSION = 1;
|
|
24
24
|
const PLUGIN_PREFERENCES_VERSION = 1;
|
|
25
|
-
const
|
|
25
|
+
const ARCHIVE_CHUNK_BYTES = 512 * 1024;
|
|
26
26
|
const MS_PER_SECOND = 1000;
|
|
27
|
+
const DEFAULT_APPLE_CONTAINER_IMAGE = "ubuntu:24.04";
|
|
27
28
|
const DEFAULT_FREESTYLE_API_URL = "https://api.freestyle.sh";
|
|
28
29
|
const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
|
|
29
30
|
const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
|
|
@@ -138,9 +139,10 @@ export async function sync(sdkOptions) {
|
|
|
138
139
|
progress.step("Reading sync cache");
|
|
139
140
|
const cache = currentCache;
|
|
140
141
|
if (!options.vmId && !cache.snapshotId) {
|
|
141
|
-
console.warn(`${CLI_NAME} --skip-sync: no cached snapshot found. A new empty
|
|
142
|
+
console.warn(`${CLI_NAME} --skip-sync: no cached snapshot found. A new empty runtime will be created. Run without --skip-sync first to create a snapshot.`);
|
|
142
143
|
}
|
|
143
|
-
const
|
|
144
|
+
const provider = resolveVmProvider(options.provider);
|
|
145
|
+
const source = options.vmId ? `existing ${provider.runtimeLabel} ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : `a new ${provider.runtimeLabel}`;
|
|
144
146
|
console.log(`Skipping sync: creating VM from ${source}`);
|
|
145
147
|
console.log(`Remote project: ${options.remoteProjectDir}`);
|
|
146
148
|
if (options.dryRun) {
|
|
@@ -149,26 +151,26 @@ export async function sync(sdkOptions) {
|
|
|
149
151
|
snapshotId: currentCache.snapshotId,
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
|
-
progress.step(
|
|
153
|
-
const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
|
|
154
|
+
progress.step(`Preparing ${provider.displayName}`);
|
|
155
|
+
const { vm, vmId, provider: runtimeProvider } = await getOrCreateVm(options, cache.snapshotId);
|
|
154
156
|
resultVmId = vmId;
|
|
155
|
-
console.log(`Using
|
|
157
|
+
console.log(`Using ${runtimeProvider.runtimeLabel}: ${vmId}`);
|
|
156
158
|
progress.step(`Running post-sync plugins for ${vmId}`);
|
|
157
159
|
const contextCandidates = await discoverPluginContextCandidates(options);
|
|
158
160
|
const postSyncMessages = await runAfterSyncPlugins(vm, vmId, options, contextCandidates);
|
|
159
161
|
console.log("");
|
|
160
|
-
console.log(
|
|
162
|
+
console.log(`${capitalize(runtimeProvider.runtimeLabel)} ready: ${vmId}`);
|
|
161
163
|
console.log(`Project: ${options.remoteProjectDir}`);
|
|
162
164
|
if (cache.snapshotId) {
|
|
163
165
|
console.log(`Snapshot cache: ${cache.snapshotId}`);
|
|
164
166
|
}
|
|
165
167
|
for (const message of postSyncMessages)
|
|
166
168
|
console.log(message);
|
|
167
|
-
console.log(
|
|
169
|
+
console.log(`${capitalize(runtimeProvider.runtimeLabel)} access: ${runtimeProvider.connectionHint(vmId)}`);
|
|
168
170
|
if (options.autoSsh) {
|
|
169
171
|
const connected = await runConnectPlugins(vm, vmId, options, contextCandidates);
|
|
170
172
|
if (!connected)
|
|
171
|
-
await
|
|
173
|
+
await connectToVm(runtimeProvider, vmId);
|
|
172
174
|
}
|
|
173
175
|
return {
|
|
174
176
|
vmId,
|
|
@@ -198,10 +200,11 @@ export async function sync(sdkOptions) {
|
|
|
198
200
|
snapshotId: currentCache.snapshotId,
|
|
199
201
|
};
|
|
200
202
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
const provider = resolveVmProvider(options.provider);
|
|
204
|
+
progress.step(`Preparing ${provider.displayName}`);
|
|
205
|
+
const { vm, vmId, provider: runtimeProvider } = await getOrCreateVm(options, cache.snapshotId);
|
|
203
206
|
resultVmId = vmId;
|
|
204
|
-
console.log(`Uploading to
|
|
207
|
+
console.log(`Uploading to ${runtimeProvider.runtimeLabel}: ${vmId}`);
|
|
205
208
|
progress.step(`Uploading project changes to ${vmId}`);
|
|
206
209
|
await ensureRemoteBase(vm, options.remoteProjectDir);
|
|
207
210
|
const projectResult = await syncProject(vm, vmId, options, projectChanges);
|
|
@@ -223,6 +226,7 @@ export async function sync(sdkOptions) {
|
|
|
223
226
|
version: CACHE_VERSION,
|
|
224
227
|
projectRoot: options.projectRoot,
|
|
225
228
|
remoteProjectDir: options.remoteProjectDir,
|
|
229
|
+
provider: options.provider,
|
|
226
230
|
vmId,
|
|
227
231
|
snapshotId: snapshotOverride ? snapshotOverride.snapshotId : cache.snapshotId,
|
|
228
232
|
projectFiles: projectCurrent,
|
|
@@ -236,12 +240,13 @@ export async function sync(sdkOptions) {
|
|
|
236
240
|
progress.step("Saving local sync cache");
|
|
237
241
|
await saveCache(buildSyncCache());
|
|
238
242
|
let snapshotPromise = null;
|
|
239
|
-
|
|
243
|
+
const createSnapshot = vm.snapshot;
|
|
244
|
+
if (options.snapshot && createSnapshot) {
|
|
240
245
|
progress.step(`Creating snapshot cache for ${vmId} in background`);
|
|
241
246
|
snapshotPromise = (async () => {
|
|
242
247
|
await runBeforeSnapshotPlugins(vm, vmId, options);
|
|
243
248
|
try {
|
|
244
|
-
const snapshot = await
|
|
249
|
+
const snapshot = await createSnapshot({ name: `${CLI_NAME}-${path.basename(options.projectRoot)}-${Date.now()}` });
|
|
245
250
|
return snapshot.snapshotId;
|
|
246
251
|
}
|
|
247
252
|
catch (error) {
|
|
@@ -250,6 +255,9 @@ export async function sync(sdkOptions) {
|
|
|
250
255
|
}
|
|
251
256
|
})();
|
|
252
257
|
}
|
|
258
|
+
else if (options.snapshot) {
|
|
259
|
+
progress.step(`Skipping snapshot cache for ${runtimeProvider.displayName}`);
|
|
260
|
+
}
|
|
253
261
|
else {
|
|
254
262
|
progress.step("Skipping snapshot cache");
|
|
255
263
|
}
|
|
@@ -257,7 +265,7 @@ export async function sync(sdkOptions) {
|
|
|
257
265
|
const postSyncMessages = await runAfterSyncPlugins(vm, vmId, options, contextCandidates);
|
|
258
266
|
progress.finish();
|
|
259
267
|
console.log("");
|
|
260
|
-
console.log(`${success(symbol("✓", "*"))} ${bold(
|
|
268
|
+
console.log(`${success(symbol("✓", "*"))} ${bold(`${capitalize(runtimeProvider.runtimeLabel)} ready: ${vmId}`)}`);
|
|
261
269
|
console.log(`${dim("Remote project:")} ${options.remoteProjectDir}`);
|
|
262
270
|
console.log(`${dim("Project files:")} ${projectResult.uploaded} uploaded, ${projectResult.removed} removed, ${projectResult.unchanged} unchanged`);
|
|
263
271
|
console.log(`${dim("Context files:")} ${contextResult.uploaded} uploaded, ${contextResult.unchanged} unchanged`);
|
|
@@ -269,11 +277,11 @@ export async function sync(sdkOptions) {
|
|
|
269
277
|
}
|
|
270
278
|
for (const message of postSyncMessages)
|
|
271
279
|
console.log(message);
|
|
272
|
-
console.log(`${accent(symbol("➜", ">"))} ${bold(
|
|
280
|
+
console.log(`${accent(symbol("➜", ">"))} ${bold(`${capitalize(runtimeProvider.runtimeLabel)} access: ${runtimeProvider.connectionHint(vmId)}`)}`);
|
|
273
281
|
if (options.autoSsh) {
|
|
274
282
|
const connected = await runConnectPlugins(vm, vmId, options, contextCandidates);
|
|
275
283
|
if (!connected)
|
|
276
|
-
await
|
|
284
|
+
await connectToVm(runtimeProvider, vmId);
|
|
277
285
|
}
|
|
278
286
|
if (snapshotPromise) {
|
|
279
287
|
const newSnapshotId = await snapshotPromise;
|
|
@@ -516,9 +524,15 @@ async function parseArgs(args) {
|
|
|
516
524
|
else if (arg === "--no-ssh") {
|
|
517
525
|
options.autoSsh = false;
|
|
518
526
|
}
|
|
527
|
+
else if (arg === "--provider" || arg === "--vm-provider") {
|
|
528
|
+
options.provider = parseVmProvider(readOptionValue(args, ++index, arg));
|
|
529
|
+
}
|
|
519
530
|
else if (arg === "--vm-id") {
|
|
520
531
|
options.vmId = readOptionValue(args, ++index, arg);
|
|
521
532
|
}
|
|
533
|
+
else if (arg === "--apple-container-image" || arg === "--container-image") {
|
|
534
|
+
options.appleContainerImage = readOptionValue(args, ++index, arg);
|
|
535
|
+
}
|
|
522
536
|
else if (arg === "--name") {
|
|
523
537
|
options.name = readOptionValue(args, ++index, arg);
|
|
524
538
|
}
|
|
@@ -565,7 +579,9 @@ function defaultCliOptions() {
|
|
|
565
579
|
projectRoot: process.cwd(),
|
|
566
580
|
cachePath: "",
|
|
567
581
|
remoteProjectDir: "",
|
|
582
|
+
provider: "freestyle",
|
|
568
583
|
name: "",
|
|
584
|
+
appleContainerImage: DEFAULT_APPLE_CONTAINER_IMAGE,
|
|
569
585
|
yes: false,
|
|
570
586
|
dryRun: false,
|
|
571
587
|
disablePlugins: [],
|
|
@@ -599,14 +615,17 @@ async function finalizeCliOptions(options) {
|
|
|
599
615
|
};
|
|
600
616
|
}
|
|
601
617
|
function printHelp() {
|
|
602
|
-
console.log(`${CLI_NAME} uploads the current project into a
|
|
618
|
+
console.log(`${CLI_NAME} uploads the current project into a remote runtime.
|
|
603
619
|
|
|
604
620
|
Usage:
|
|
605
621
|
${CLI_NAME} [project-dir] [options]
|
|
606
622
|
|
|
607
623
|
Options:
|
|
608
|
-
|
|
609
|
-
|
|
624
|
+
--provider <name> Runtime provider: freestyle or apple-container. Defaults to freestyle.
|
|
625
|
+
--vm-id <id> Sync into an existing provider runtime.
|
|
626
|
+
--apple-container-image <image>
|
|
627
|
+
Apple Containers image. Defaults to ${DEFAULT_APPLE_CONTAINER_IMAGE}.
|
|
628
|
+
--name <name> Name for a newly created runtime. Defaults to ${CLI_NAME}-<project>.
|
|
610
629
|
--remote-dir <path> Remote project directory. Defaults to the local absolute path.
|
|
611
630
|
--cache <path> Snapshot/hash cache path. Defaults to .freestyle-sync/cache.json.
|
|
612
631
|
--include-env <name> Always copy an environment variable. Repeatable.
|
|
@@ -616,16 +635,16 @@ Options:
|
|
|
616
635
|
--list-plugins Show configured plugins and whether they are enabled.
|
|
617
636
|
--install Run detected dependency install command after sync.
|
|
618
637
|
--no-ssh Do not automatically open VS Code/Cursor or SSH after sync.
|
|
619
|
-
|
|
638
|
+
--idle-timeout <seconds> Set runtime idle timeout when supported.
|
|
620
639
|
--no-auth Disable auth plugins for this run.
|
|
621
640
|
--no-agent-context Disable agent context plugins for this run.
|
|
622
641
|
--no-git-dir Exclude the local .git directory from project sync.
|
|
623
642
|
--all-copilot-workspaces Include Copilot chat state for every VS Code workspace.
|
|
624
643
|
--no-snapshot, --skip-snapshot
|
|
625
|
-
Do not snapshot the
|
|
626
|
-
--skip-sync Create a
|
|
627
|
-
files. Requires a
|
|
628
|
-
--dry-run Show what would sync without creating or changing a
|
|
644
|
+
Do not snapshot the runtime after sync.
|
|
645
|
+
--skip-sync Create a runtime from the last cached snapshot without syncing current
|
|
646
|
+
files. Requires a provider that supports snapshots, or --vm-id.
|
|
647
|
+
--dry-run Show what would sync without creating or changing a runtime.
|
|
629
648
|
-y, --yes Deprecated; accepted for compatibility.
|
|
630
649
|
-h, --help Show this help.
|
|
631
650
|
`);
|
|
@@ -640,9 +659,11 @@ function readOptionValue(args, index, option) {
|
|
|
640
659
|
async function readCache(cachePath, options) {
|
|
641
660
|
try {
|
|
642
661
|
const parsed = JSON.parse(await readFile(cachePath, "utf8"));
|
|
643
|
-
|
|
662
|
+
const provider = parsed.provider ?? "freestyle";
|
|
663
|
+
if (parsed.version === CACHE_VERSION && parsed.projectRoot === options.projectRoot && parsed.remoteProjectDir === options.remoteProjectDir && provider === options.provider) {
|
|
644
664
|
return {
|
|
645
665
|
...parsed,
|
|
666
|
+
provider,
|
|
646
667
|
projectFiles: parsed.projectFiles ?? {},
|
|
647
668
|
contextFiles: parsed.contextFiles ?? {},
|
|
648
669
|
snapshotProjectFiles: parsed.snapshotProjectFiles,
|
|
@@ -659,6 +680,7 @@ async function readCache(cachePath, options) {
|
|
|
659
680
|
version: CACHE_VERSION,
|
|
660
681
|
projectRoot: options.projectRoot,
|
|
661
682
|
remoteProjectDir: options.remoteProjectDir,
|
|
683
|
+
provider: options.provider,
|
|
662
684
|
projectFiles: {},
|
|
663
685
|
contextFiles: {},
|
|
664
686
|
};
|
|
@@ -669,6 +691,7 @@ function normalizeCache(cache, options) {
|
|
|
669
691
|
version: CACHE_VERSION,
|
|
670
692
|
projectRoot: options.projectRoot,
|
|
671
693
|
remoteProjectDir: options.remoteProjectDir,
|
|
694
|
+
provider: options.provider,
|
|
672
695
|
projectFiles: cache.projectFiles ?? {},
|
|
673
696
|
contextFiles: cache.contextFiles ?? {},
|
|
674
697
|
snapshotProjectFiles: cache.snapshotProjectFiles ?? {},
|
|
@@ -962,9 +985,11 @@ async function writePluginPreferences(preferencesPath, preferences) {
|
|
|
962
985
|
await writeFile(preferencesPath, `${JSON.stringify({ ...preferences, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
963
986
|
}
|
|
964
987
|
function printPlan(options, projectChanges, contextChanges, envExports, cache) {
|
|
965
|
-
const
|
|
988
|
+
const provider = resolveVmProvider(options.provider);
|
|
989
|
+
const source = options.vmId ? `existing ${provider.runtimeLabel} ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : `a new ${provider.runtimeLabel}`;
|
|
966
990
|
console.log("");
|
|
967
991
|
console.log(`${bold("Sync plan")}`);
|
|
992
|
+
console.log(`${dim(" Provider:")} ${provider.displayName}`);
|
|
968
993
|
console.log(`${dim(" Source:")} ${source}`);
|
|
969
994
|
console.log(`${dim(" Local:")} ${options.projectRoot}`);
|
|
970
995
|
console.log(`${dim(" Remote:")} ${options.remoteProjectDir}`);
|
|
@@ -1200,20 +1225,161 @@ async function runFreestyleLogin(extraArgs = []) {
|
|
|
1200
1225
|
});
|
|
1201
1226
|
});
|
|
1202
1227
|
}
|
|
1228
|
+
function parseVmProvider(value) {
|
|
1229
|
+
if (value === "freestyle" || value === "apple-container")
|
|
1230
|
+
return value;
|
|
1231
|
+
throw new Error(`unknown provider: ${value}. Expected freestyle or apple-container.`);
|
|
1232
|
+
}
|
|
1233
|
+
function resolveVmProvider(provider) {
|
|
1234
|
+
if (provider === "apple-container")
|
|
1235
|
+
return appleContainerProvider;
|
|
1236
|
+
return freestyleVmProvider;
|
|
1237
|
+
}
|
|
1238
|
+
const freestyleVmProvider = {
|
|
1239
|
+
name: "freestyle",
|
|
1240
|
+
displayName: "Freestyle VM",
|
|
1241
|
+
runtimeLabel: "VM",
|
|
1242
|
+
async prepare(options, snapshotId) {
|
|
1243
|
+
const freestyle = await getFreestyleClient();
|
|
1244
|
+
if (options.vmId) {
|
|
1245
|
+
const { vm } = await freestyle.vms.get({ vmId: options.vmId });
|
|
1246
|
+
await vm.start(options.idleTimeoutSeconds ? { idleTimeoutSeconds: options.idleTimeoutSeconds } : undefined);
|
|
1247
|
+
return { vm: vm, vmId: options.vmId };
|
|
1248
|
+
}
|
|
1249
|
+
console.log(snapshotId ? `Creating VM from snapshot ${snapshotId}...` : "Creating Freestyle VM...");
|
|
1250
|
+
const result = await freestyle.vms.create({
|
|
1251
|
+
name: options.name,
|
|
1252
|
+
snapshotId: snapshotId ?? undefined,
|
|
1253
|
+
idleTimeoutSeconds: options.idleTimeoutSeconds,
|
|
1254
|
+
});
|
|
1255
|
+
return { vm: result.vm, vmId: result.vmId };
|
|
1256
|
+
},
|
|
1257
|
+
connectionHint(vmId) {
|
|
1258
|
+
return `npx freestyle vm ssh ${vmId}`;
|
|
1259
|
+
},
|
|
1260
|
+
async connect(vmId) {
|
|
1261
|
+
console.log(`Connecting to VM ${vmId}...`);
|
|
1262
|
+
await spawnInherited("npx", ["freestyle", "vm", "ssh", vmId], "ssh exited with status");
|
|
1263
|
+
},
|
|
1264
|
+
};
|
|
1265
|
+
const appleContainerProvider = {
|
|
1266
|
+
name: "apple-container",
|
|
1267
|
+
displayName: "Apple container",
|
|
1268
|
+
runtimeLabel: "container",
|
|
1269
|
+
async prepare(options) {
|
|
1270
|
+
if (options.vmId) {
|
|
1271
|
+
await startAppleContainer(options.vmId);
|
|
1272
|
+
return { vm: new AppleContainerVm(options.vmId), vmId: options.vmId };
|
|
1273
|
+
}
|
|
1274
|
+
console.log(`Creating Apple container ${options.name} from ${options.appleContainerImage}...`);
|
|
1275
|
+
const result = await execFileAsync("container", ["run", "--detach", "--name", options.name, options.appleContainerImage, "sleep", "infinity"]);
|
|
1276
|
+
const vmId = result.stdout.trim() || options.name;
|
|
1277
|
+
return { vm: new AppleContainerVm(vmId), vmId };
|
|
1278
|
+
},
|
|
1279
|
+
connectionHint(vmId) {
|
|
1280
|
+
return `container exec --interactive --tty ${vmId} sh -l`;
|
|
1281
|
+
},
|
|
1282
|
+
async connect(vmId) {
|
|
1283
|
+
console.log(`Connecting to container ${vmId}...`);
|
|
1284
|
+
await spawnInherited("container", ["exec", "--interactive", "--tty", vmId, "sh", "-l"], "container shell exited with status");
|
|
1285
|
+
},
|
|
1286
|
+
};
|
|
1203
1287
|
async function getOrCreateVm(options, snapshotId) {
|
|
1204
|
-
const
|
|
1205
|
-
if (
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1288
|
+
const provider = resolveVmProvider(options.provider);
|
|
1289
|
+
if (snapshotId && provider.name !== "freestyle") {
|
|
1290
|
+
console.warn(`${provider.displayName} does not support Freestyle snapshots. Creating a fresh ${provider.runtimeLabel}.`);
|
|
1291
|
+
}
|
|
1292
|
+
const runtime = await provider.prepare(options, provider.name === "freestyle" ? snapshotId : undefined);
|
|
1293
|
+
return { ...runtime, provider };
|
|
1294
|
+
}
|
|
1295
|
+
async function startAppleContainer(containerId) {
|
|
1296
|
+
const result = await execFileResult("container", ["start", containerId]);
|
|
1297
|
+
if (result.statusCode !== 0 && !/already running/i.test(result.stderr || result.stdout || "")) {
|
|
1298
|
+
throw new Error(`failed to start Apple container ${containerId}: ${result.stderr || result.stdout || `exit ${result.statusCode}`}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
class AppleContainerVm {
|
|
1302
|
+
containerId;
|
|
1303
|
+
fs = {
|
|
1304
|
+
writeFile: (remotePath, content) => this.writeFile(remotePath, content),
|
|
1305
|
+
writeTextFile: (remotePath, content) => this.writeFile(remotePath, Buffer.from(content)),
|
|
1306
|
+
};
|
|
1307
|
+
constructor(containerId) {
|
|
1308
|
+
this.containerId = containerId;
|
|
1309
|
+
}
|
|
1310
|
+
exec(args) {
|
|
1311
|
+
return execFileResult("container", ["exec", this.containerId, "sh", "-lc", args.command], { timeoutMs: args.timeoutMs });
|
|
1312
|
+
}
|
|
1313
|
+
async writeFile(remotePath, content) {
|
|
1314
|
+
const directory = path.posix.dirname(remotePath);
|
|
1315
|
+
await checkedContainerExec(this.containerId, `mkdir -p ${shellQuote(directory)} && cat > ${shellQuote(remotePath)}`, content);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
async function checkedContainerExec(containerId, command, input) {
|
|
1319
|
+
const result = await execFileResult("container", ["exec", "--interactive", containerId, "sh", "-lc", command], { input });
|
|
1320
|
+
if (result.statusCode && result.statusCode !== 0) {
|
|
1321
|
+
throw new Error(`container command failed (${result.statusCode}): ${command}\n${result.stderr || result.stdout || ""}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function execFileResult(file, args, options = {}) {
|
|
1325
|
+
return new Promise((resolve, reject) => {
|
|
1326
|
+
const child = spawn(file, args, { stdio: [options.input ? "pipe" : "ignore", "pipe", "pipe"] });
|
|
1327
|
+
const stdout = [];
|
|
1328
|
+
const stderr = [];
|
|
1329
|
+
let settled = false;
|
|
1330
|
+
let timedOut = false;
|
|
1331
|
+
const timer = options.timeoutMs
|
|
1332
|
+
? setTimeout(() => {
|
|
1333
|
+
timedOut = true;
|
|
1334
|
+
child.kill("SIGTERM");
|
|
1335
|
+
}, options.timeoutMs)
|
|
1336
|
+
: undefined;
|
|
1337
|
+
child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
1338
|
+
child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
1339
|
+
child.on("error", (error) => {
|
|
1340
|
+
if (settled)
|
|
1341
|
+
return;
|
|
1342
|
+
settled = true;
|
|
1343
|
+
if (timer)
|
|
1344
|
+
clearTimeout(timer);
|
|
1345
|
+
reject(error);
|
|
1346
|
+
});
|
|
1347
|
+
child.on("exit", (code) => {
|
|
1348
|
+
if (settled)
|
|
1349
|
+
return;
|
|
1350
|
+
settled = true;
|
|
1351
|
+
if (timer)
|
|
1352
|
+
clearTimeout(timer);
|
|
1353
|
+
resolve({
|
|
1354
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
1355
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1356
|
+
statusCode: timedOut ? 124 : code ?? 1,
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
if (options.input) {
|
|
1360
|
+
child.stdin?.end(options.input);
|
|
1361
|
+
}
|
|
1215
1362
|
});
|
|
1216
|
-
|
|
1363
|
+
}
|
|
1364
|
+
async function spawnInherited(file, args, errorPrefix) {
|
|
1365
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
1366
|
+
const child = spawn(file, args, { stdio: "inherit" });
|
|
1367
|
+
child.on("error", reject);
|
|
1368
|
+
child.on("exit", (code) => resolve(code));
|
|
1369
|
+
});
|
|
1370
|
+
if (exitCode && exitCode !== 0) {
|
|
1371
|
+
throw new Error(`${errorPrefix} ${exitCode}`);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async function connectToVm(provider, vmId) {
|
|
1375
|
+
if (!provider.connect) {
|
|
1376
|
+
console.log(`Connect with: ${provider.connectionHint(vmId)}`);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
await provider.connect(vmId);
|
|
1380
|
+
}
|
|
1381
|
+
function capitalize(value) {
|
|
1382
|
+
return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
1217
1383
|
}
|
|
1218
1384
|
async function ensureRemoteBase(vm, remoteProjectDir) {
|
|
1219
1385
|
await checkedExec(vm, `mkdir -p ${shellQuote(remoteProjectDir)} /root/.freestyle-sync`);
|
|
@@ -1392,20 +1558,20 @@ async function createTar(args) {
|
|
|
1392
1558
|
});
|
|
1393
1559
|
}
|
|
1394
1560
|
async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, label) {
|
|
1395
|
-
const
|
|
1396
|
-
const
|
|
1397
|
-
const chunkCount = Math.max(1, Math.ceil(
|
|
1561
|
+
const archiveSize = (await stat(archivePath)).size;
|
|
1562
|
+
const archiveHash = await hashFile(archivePath);
|
|
1563
|
+
const chunkCount = Math.max(1, Math.ceil(archiveSize / ARCHIVE_CHUNK_BYTES));
|
|
1398
1564
|
const chunkDir = `/tmp/freestyle-sync-${label}-${Date.now()}.chunks`;
|
|
1399
|
-
console.log(`VM ${vmId}: streaming ${formatBytes(
|
|
1565
|
+
console.log(`VM ${vmId}: streaming ${formatBytes(archiveSize)} ${label} archive in ${chunkCount} chunk${chunkCount === 1 ? "" : "s"}...`);
|
|
1400
1566
|
await checkedExec(vm, `rm -rf ${shellQuote(chunkDir)} && mkdir -p ${shellQuote(chunkDir)}`);
|
|
1401
1567
|
const width = String(chunkCount - 1).length;
|
|
1402
1568
|
const canRenderInlineProgress = process.stdout.isTTY;
|
|
1403
1569
|
const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
const
|
|
1407
|
-
const
|
|
1408
|
-
await vm.fs.writeTextFile(`${chunkDir}/${chunkName}`,
|
|
1570
|
+
let index = 0;
|
|
1571
|
+
for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
|
|
1572
|
+
const chunkName = `${String(index).padStart(width, "0")}.chunk.b64`;
|
|
1573
|
+
const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1574
|
+
await vm.fs.writeTextFile(`${chunkDir}/${chunkName}`, content.toString("base64"));
|
|
1409
1575
|
const uploadedChunks = index + 1;
|
|
1410
1576
|
if (chunkCount > 1) {
|
|
1411
1577
|
const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
|
|
@@ -1419,8 +1585,27 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
1419
1585
|
console.log(progressMessage);
|
|
1420
1586
|
}
|
|
1421
1587
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1588
|
+
index += 1;
|
|
1589
|
+
}
|
|
1590
|
+
await checkedExec(vm, archiveSize === 0
|
|
1591
|
+
? `: > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`
|
|
1592
|
+
: [
|
|
1593
|
+
"set -e",
|
|
1594
|
+
`rm -f ${shellQuote(remoteArchivePath)}`,
|
|
1595
|
+
`for chunk in ${shellQuote(chunkDir)}/*.chunk.b64; do`,
|
|
1596
|
+
` base64 -d "$chunk" >> ${shellQuote(remoteArchivePath)}`,
|
|
1597
|
+
"done",
|
|
1598
|
+
`actual_size=$(wc -c < ${shellQuote(remoteArchivePath)} | tr -d '[:space:]')`,
|
|
1599
|
+
`actual_hash=$(sha256sum ${shellQuote(remoteArchivePath)} | awk '{print $1}')`,
|
|
1600
|
+
`if [ "$actual_size" != ${shellQuote(String(archiveSize))} ] || [ "$actual_hash" != ${shellQuote(archiveHash)} ]; then`,
|
|
1601
|
+
` rm -f ${shellQuote(remoteArchivePath)}`,
|
|
1602
|
+
` rm -rf ${shellQuote(chunkDir)}`,
|
|
1603
|
+
` echo ${shellQuote(`archive integrity check failed for ${label}: expected ${archiveSize} bytes/${archiveHash}`)} >&2`,
|
|
1604
|
+
` echo "got $actual_size bytes/$actual_hash" >&2`,
|
|
1605
|
+
" exit 1",
|
|
1606
|
+
"fi",
|
|
1607
|
+
`rm -rf ${shellQuote(chunkDir)}`,
|
|
1608
|
+
].join("\n"));
|
|
1424
1609
|
}
|
|
1425
1610
|
async function mkdirRemote(vm, directories) {
|
|
1426
1611
|
for (const chunk of chunkArray(directories, 50)) {
|
|
@@ -1487,17 +1672,6 @@ async function runConnectPlugins(vm, vmId, options, contextCandidates) {
|
|
|
1487
1672
|
}
|
|
1488
1673
|
return false;
|
|
1489
1674
|
}
|
|
1490
|
-
async function sshIntoVm(vmId) {
|
|
1491
|
-
console.log(`Connecting to VM ${vmId}...`);
|
|
1492
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
1493
|
-
const child = spawn("npx", ["freestyle", "vm", "ssh", vmId], { stdio: "inherit" });
|
|
1494
|
-
child.on("error", reject);
|
|
1495
|
-
child.on("exit", (code) => resolve(code));
|
|
1496
|
-
});
|
|
1497
|
-
if (exitCode && exitCode !== 0) {
|
|
1498
|
-
throw new Error(`ssh exited with status ${exitCode}`);
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
1675
|
async function hashFile(filePath) {
|
|
1502
1676
|
const hash = createHash("sha256");
|
|
1503
1677
|
await new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freestyle-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/main.js",
|
|
6
6
|
"exports": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"build": "rm -rf dist plugins/*/dist && tsc -p tsconfig.json && node scripts/prepare-plugin-packages.mjs",
|
|
23
23
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
24
24
|
"prepack": "npm run build",
|
|
25
|
-
"publish": "npm run check && npm run build && npm run publish:workspaces && npm publish",
|
|
25
|
+
"publish": "npm run check && npm run build && npm run publish:workspaces && npm run publish:root",
|
|
26
|
+
"publish:root": "node scripts/publish-root.mjs",
|
|
26
27
|
"publish:workspaces": "node scripts/publish-workspaces.mjs",
|
|
27
28
|
"start": "node src/main.ts"
|
|
28
29
|
},
|