freestyle-sync 0.1.3 → 0.1.4

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.
Files changed (44) hide show
  1. package/{freestyle-sync.config.ts → dist/freestyle-sync.config.js} +2 -3
  2. package/dist/main.js +1319 -0
  3. package/{plugins/agent-claude/src/index.ts → dist/plugins/agent-claude/src/index.js} +32 -29
  4. package/{plugins/agent-codex/src/index.ts → dist/plugins/agent-codex/src/index.js} +13 -14
  5. package/{plugins/agent-copilot/src/index.ts → dist/plugins/agent-copilot/src/index.js} +136 -159
  6. package/{plugins/auth-aws/src/index.ts → dist/plugins/auth-aws/src/index.js} +14 -8
  7. package/{plugins/auth-azure/src/index.ts → dist/plugins/auth-azure/src/index.js} +14 -8
  8. package/dist/plugins/auth-context.js +213 -0
  9. package/{plugins/auth-docker/src/index.ts → dist/plugins/auth-docker/src/index.js} +14 -8
  10. package/{plugins/auth-env/src/index.ts → dist/plugins/auth-env/src/index.js} +11 -11
  11. package/{plugins/auth-gcloud/src/index.ts → dist/plugins/auth-gcloud/src/index.js} +14 -8
  12. package/{plugins/auth-git/src/index.ts → dist/plugins/auth-git/src/index.js} +24 -17
  13. package/{plugins/auth-github-cli/src/index.ts → dist/plugins/auth-github-cli/src/index.js} +20 -14
  14. package/{plugins/auth-npm/src/index.ts → dist/plugins/auth-npm/src/index.js} +19 -13
  15. package/{plugins/auth-ssh/src/index.ts → dist/plugins/auth-ssh/src/index.js} +19 -13
  16. package/dist/plugins/auth-yarn/src/index.js +24 -0
  17. package/{plugins/node-npm/src/index.ts → dist/plugins/node-npm/src/index.js} +6 -8
  18. package/dist/plugins/npm-native-deps.js +307 -0
  19. package/{plugins/shell-history/src/index.ts → dist/plugins/shell-history/src/index.js} +13 -12
  20. package/{plugins/vscode/src/index.ts → dist/plugins/vscode/src/index.js} +38 -40
  21. package/{src/main.ts → dist/src/main.js} +285 -464
  22. package/dist/src/plugin-api.js +6 -0
  23. package/dist/src/pushvm.config.js +36 -0
  24. package/package.json +8 -4
  25. package/PUBLISHING.md +0 -3
  26. package/plugins/agent-claude/package.json +0 -8
  27. package/plugins/agent-codex/package.json +0 -8
  28. package/plugins/agent-copilot/package.json +0 -8
  29. package/plugins/auth-aws/package.json +0 -8
  30. package/plugins/auth-azure/package.json +0 -8
  31. package/plugins/auth-docker/package.json +0 -8
  32. package/plugins/auth-env/package.json +0 -8
  33. package/plugins/auth-gcloud/package.json +0 -8
  34. package/plugins/auth-git/package.json +0 -8
  35. package/plugins/auth-github-cli/package.json +0 -8
  36. package/plugins/auth-npm/package.json +0 -8
  37. package/plugins/auth-ssh/package.json +0 -8
  38. package/plugins/auth-yarn/package.json +0 -8
  39. package/plugins/auth-yarn/src/index.ts +0 -19
  40. package/plugins/node-npm/package.json +0 -8
  41. package/plugins/shell-history/package.json +0 -8
  42. package/plugins/vscode/package.json +0 -8
  43. package/src/plugin-api.ts +0 -110
  44. package/tsconfig.json +0 -18
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import "dotenv/config"
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
10
+ import "dotenv/config";
3
11
  import { createHash } from "node:crypto";
4
12
  import { createReadStream, realpathSync } from "node:fs";
5
13
  import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
@@ -9,20 +17,17 @@ import { fileURLToPath, pathToFileURL } from "node:url";
9
17
  import { execFile, spawn } from "node:child_process";
10
18
  import { promisify } from "node:util";
11
19
  import { freestyle } from "freestyle";
12
- import type { CliOptions, ContextCandidate, PushvmConfig, PushvmPluginUtils, RemoteVm } from "./plugin-api.ts";
13
- export * from "./plugin-api.ts";
14
-
20
+ export * from "./plugin-api.js";
15
21
  const execFileAsync = promisify(execFile);
16
-
17
22
  const CACHE_VERSION = 1;
18
23
  const PLUGIN_PREFERENCES_VERSION = 1;
19
24
  const ARCHIVE_CHUNK_CHARS = 1024 * 1024;
20
25
  const MS_PER_SECOND = 1000;
21
26
  const USE_UNICODE_OUTPUT = process.stdout.isTTY && (process.env.TERM !== "dumb" || Boolean(process.env.TERM_PROGRAM));
22
27
  const USE_STYLED_OUTPUT = process.stdout.isTTY && process.env.NO_COLOR !== "1";
23
- let config: PushvmConfig = { plugins: [] };
28
+ let config = { plugins: [] };
24
29
  let plugins = config.plugins;
25
- const pluginUtils: PushvmPluginUtils = {
30
+ const pluginUtils = {
26
31
  checkedExec,
27
32
  createTar,
28
33
  uploadArchiveInChunks,
@@ -31,65 +36,15 @@ const pluginUtils: PushvmPluginUtils = {
31
36
  md5,
32
37
  delay,
33
38
  };
34
-
35
- type FileKind = "file" | "symlink";
36
-
37
- type FileDigest = {
38
- hash: string;
39
- kind: FileKind;
40
- mode: number;
41
- size: number;
42
- };
43
-
44
- type LocalEntry = FileDigest & {
45
- absolutePath: string;
46
- relativePath: string;
47
- };
48
-
49
- type CacheFile = {
50
- version: number;
51
- projectRoot: string;
52
- remoteProjectDir: string;
53
- vmId?: string;
54
- snapshotId?: string;
55
- projectFiles: Record<string, FileDigest>;
56
- contextFiles: Record<string, FileDigest>;
57
- snapshotProjectFiles?: Record<string, FileDigest>;
58
- snapshotContextFiles?: Record<string, FileDigest>;
59
- envHash?: string;
60
- snapshotEnvHash?: string;
61
- updatedAt?: string;
62
- };
63
-
64
- type PluginPreferences = {
65
- version: number;
66
- disabledPlugins: string[];
67
- updatedAt?: string;
68
- };
69
-
70
- type ContextEntry = LocalEntry & {
71
- remotePath: string;
72
- label: string;
73
- sensitive: boolean;
74
- };
75
-
76
- type SyncResult = {
77
- uploaded: number;
78
- removed: number;
79
- unchanged: number;
80
- };
81
-
82
39
  class Progress {
83
- private current = 0;
84
- private readonly total: number;
85
- private currentStepStartedAt = 0;
86
- private currentStepMessage?: string;
87
-
88
- constructor(total: number) {
40
+ current = 0;
41
+ total;
42
+ currentStepStartedAt = 0;
43
+ currentStepMessage;
44
+ constructor(total) {
89
45
  this.total = total;
90
46
  }
91
-
92
- step(message: string) {
47
+ step(message) {
93
48
  if (this.currentStepMessage) {
94
49
  this.printCompletion();
95
50
  }
@@ -98,27 +53,25 @@ class Progress {
98
53
  this.currentStepStartedAt = Date.now();
99
54
  console.log(`${accent(symbol("➜", ">"))} ${bold(`[${this.current}/${this.total}]`)} ${message}`);
100
55
  }
101
-
102
56
  finish() {
103
- if (!this.currentStepMessage) return;
57
+ if (!this.currentStepMessage)
58
+ return;
104
59
  this.printCompletion();
105
60
  this.currentStepMessage = undefined;
106
61
  }
107
-
108
- private printCompletion() {
109
- if (!this.currentStepMessage) return;
62
+ printCompletion() {
63
+ if (!this.currentStepMessage)
64
+ return;
110
65
  const elapsed = Date.now() - this.currentStepStartedAt;
111
66
  console.log(`${success(symbol("✔", "*"))} ${dim(`[${this.current}/${this.total}]`)} ${this.currentStepMessage} ${dim(formatDuration(elapsed))}`);
112
67
  }
113
68
  }
114
-
115
69
  if (isDirectCliExecution()) {
116
70
  main().catch((error) => {
117
71
  console.error(`vmpush: ${error instanceof Error ? error.message : String(error)}`);
118
72
  process.exitCode = 1;
119
73
  });
120
74
  }
121
-
122
75
  async function main() {
123
76
  const options = await parseArgs(process.argv.slice(2));
124
77
  printHeading("vmpush");
@@ -130,104 +83,84 @@ async function main() {
130
83
  printPlugins(pluginPreferences, options);
131
84
  return;
132
85
  }
133
-
134
86
  if (options.dryRun) {
135
87
  console.log("vmpush dry run");
136
88
  }
137
-
138
89
  if (options.skipSync) {
139
90
  const progress = new Progress(3);
140
91
  progress.step("Reading sync cache");
141
92
  const cache = await readCache(options.cachePath, options);
142
-
143
93
  if (!options.vmId && !cache.snapshotId) {
144
94
  console.warn("vmpush --skip-sync: no cached snapshot found. A new empty VM will be created. Run without --skip-sync first to create a snapshot.");
145
95
  }
146
-
147
96
  const source = options.vmId ? `existing VM ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : "a new VM";
148
97
  console.log(`Skipping sync: creating VM from ${source}`);
149
98
  console.log(`Remote project: ${options.remoteProjectDir}`);
150
-
151
99
  if (options.dryRun) {
152
100
  return;
153
101
  }
154
-
155
102
  progress.step("Preparing Freestyle VM");
156
103
  const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
157
104
  console.log(`Using VM: ${vmId}`);
158
-
159
105
  progress.step(`Running post-sync plugins for ${vmId}`);
160
106
  const contextCandidates = await discoverPluginContextCandidates(options);
161
107
  const postSyncMessages = await runAfterSyncPlugins(vm, vmId, options, contextCandidates);
162
-
163
108
  console.log("");
164
109
  console.log(`VM ready: ${vmId}`);
165
110
  console.log(`Project: ${options.remoteProjectDir}`);
166
111
  if (cache.snapshotId) {
167
112
  console.log(`Snapshot cache: ${cache.snapshotId}`);
168
113
  }
169
- for (const message of postSyncMessages) console.log(message);
114
+ for (const message of postSyncMessages)
115
+ console.log(message);
170
116
  console.log(`SSH: npx freestyle vm ssh ${vmId}`);
171
117
  if (options.autoSsh) {
172
118
  const connected = await runConnectPlugins(vm, vmId, options, contextCandidates);
173
- if (!connected) await sshIntoVm(vmId);
119
+ if (!connected)
120
+ await sshIntoVm(vmId);
174
121
  }
175
122
  return;
176
123
  }
177
-
178
124
  const progress = new Progress(options.dryRun ? 2 : options.install ? 12 : 11);
179
-
180
125
  progress.step("Scanning project files");
181
126
  const cache = await readCache(options.cachePath, options);
182
127
  const base = cacheBaseForSync(options, cache);
183
128
  const projectEntries = await scanProject(options.projectRoot, options.includeGitDir);
184
129
  const projectCurrent = digestMap(projectEntries);
185
130
  const projectChanges = diffEntries(projectEntries, base.projectFiles);
186
-
187
131
  progress.step("Detecting auth and agent context");
188
132
  let envExports = collectPluginEnvironment(options);
189
133
  let contextCandidates = await discoverPluginContextCandidates(options);
190
134
  const envHash = hashString(renderEnvFile(envExports));
191
-
192
135
  const contextEntries = await scanContextCandidates(contextCandidates);
193
136
  const contextCurrent = digestMapByRemotePath(contextEntries);
194
137
  const contextChanges = diffContextEntries(contextEntries, base.contextFiles);
195
-
196
138
  printPlan(options, projectChanges, contextChanges, envExports, cache);
197
-
198
139
  if (options.dryRun) {
199
140
  progress.finish();
200
141
  return;
201
142
  }
202
-
203
143
  progress.step("Preparing Freestyle VM");
204
144
  const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
205
145
  console.log(`Uploading to VM: ${vmId}`);
206
-
207
146
  progress.step(`Uploading project changes to ${vmId}`);
208
147
  await ensureRemoteBase(vm, options.remoteProjectDir);
209
148
  const projectResult = await syncProject(vm, vmId, options, projectChanges);
210
-
211
149
  progress.step(`Running remote fixups on ${vmId}`);
212
150
  await runRemoteFixups(vm, vmId, options);
213
-
214
151
  progress.step(`Uploading auth/context changes to ${vmId}`);
215
152
  const contextResult = await syncContext(vm, vmId, contextChanges);
216
153
  await runAfterContextSyncPlugins(vm, vmId, options, contextChanges.changed.map((entry) => entry.remotePath));
217
-
218
154
  progress.step(`Installing environment files on ${vmId}`);
219
155
  await installEnvironment(vm, envExports, envHash, base.envHash);
220
-
221
156
  progress.step(`Writing resume metadata on ${vmId}`);
222
157
  await writeRemoteResumeFiles(vm, options, vmId, envExports);
223
158
  await hardenRemoteRoot(vm);
224
-
225
159
  if (options.install) {
226
160
  progress.step(`Running install command on ${vmId}`);
227
161
  await runInstall(vm, options.projectRoot, options.remoteProjectDir);
228
162
  }
229
-
230
- const buildSyncCache = (snapshotOverride?: { snapshotId: string }): CacheFile => ({
163
+ const buildSyncCache = (snapshotOverride) => ({
231
164
  version: CACHE_VERSION,
232
165
  projectRoot: options.projectRoot,
233
166
  remoteProjectDir: options.remoteProjectDir,
@@ -241,11 +174,9 @@ async function main() {
241
174
  snapshotEnvHash: snapshotOverride ? envHash : cache.snapshotEnvHash,
242
175
  updatedAt: new Date().toISOString(),
243
176
  });
244
-
245
177
  progress.step("Saving local sync cache");
246
178
  await writeCache(options.cachePath, buildSyncCache());
247
-
248
- let snapshotPromise: Promise<string | null> | null = null;
179
+ let snapshotPromise = null;
249
180
  if (options.snapshot) {
250
181
  progress.step(`Creating snapshot cache for ${vmId} in background`);
251
182
  snapshotPromise = (async () => {
@@ -253,19 +184,19 @@ async function main() {
253
184
  try {
254
185
  const snapshot = await vm.snapshot({ name: `vmpush-${path.basename(options.projectRoot)}-${Date.now()}` });
255
186
  return snapshot.snapshotId;
256
- } catch (error) {
187
+ }
188
+ catch (error) {
257
189
  console.warn(`Snapshot cache skipped: ${error instanceof Error ? error.message : String(error)}`);
258
190
  return null;
259
191
  }
260
192
  })();
261
- } else {
193
+ }
194
+ else {
262
195
  progress.step("Skipping snapshot cache");
263
196
  }
264
-
265
197
  progress.step(`Running post-sync plugins for ${vmId}`);
266
198
  const postSyncMessages = await runAfterSyncPlugins(vm, vmId, options, contextCandidates);
267
199
  progress.finish();
268
-
269
200
  console.log("");
270
201
  console.log(`${success(symbol("✓", "*"))} ${bold(`VM ready: ${vmId}`)}`);
271
202
  console.log(`${dim("Remote project:")} ${options.remoteProjectDir}`);
@@ -273,16 +204,18 @@ async function main() {
273
204
  console.log(`${dim("Context files:")} ${contextResult.uploaded} uploaded, ${contextResult.unchanged} unchanged`);
274
205
  if (snapshotPromise) {
275
206
  console.log(`${dim("Snapshot cache:")} creating in background`);
276
- } else if (cache.snapshotId) {
207
+ }
208
+ else if (cache.snapshotId) {
277
209
  console.log(`${dim("Snapshot cache:")} ${cache.snapshotId}`);
278
210
  }
279
- for (const message of postSyncMessages) console.log(message);
211
+ for (const message of postSyncMessages)
212
+ console.log(message);
280
213
  console.log(`${accent(symbol("➜", ">"))} ${bold(`SSH: npx freestyle vm ssh ${vmId}`)}`);
281
214
  if (options.autoSsh) {
282
215
  const connected = await runConnectPlugins(vm, vmId, options, contextCandidates);
283
- if (!connected) await sshIntoVm(vmId);
216
+ if (!connected)
217
+ await sshIntoVm(vmId);
284
218
  }
285
-
286
219
  if (snapshotPromise) {
287
220
  const newSnapshotId = await snapshotPromise;
288
221
  if (newSnapshotId) {
@@ -291,31 +224,29 @@ async function main() {
291
224
  }
292
225
  }
293
226
  }
294
-
295
227
  function isDirectCliExecution() {
296
- if (!process.argv[1]) return false;
228
+ if (!process.argv[1])
229
+ return false;
297
230
  try {
298
231
  return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
299
- } catch {
232
+ }
233
+ catch {
300
234
  return path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
301
235
  }
302
236
  }
303
-
304
- async function loadConfig(projectRoot: string): Promise<PushvmConfig> {
237
+ async function loadConfig(projectRoot) {
305
238
  const configPath = path.join(projectRoot, "freestyle-sync.config.ts");
306
239
  if (!(await exists(configPath))) {
307
240
  await writeFile(configPath, renderDefaultConfig(), "utf8");
308
241
  console.log(`Created ${path.relative(process.cwd(), configPath) || path.basename(configPath)}`);
309
242
  }
310
-
311
- const imported = await import(pathToFileURL(configPath).href);
312
- const loaded = imported.default as PushvmConfig | undefined;
243
+ const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
244
+ const loaded = imported.default;
313
245
  if (!loaded || !Array.isArray(loaded.plugins)) {
314
246
  throw new Error(`${configPath} must export default defineConfig({ plugins: [...] })`);
315
247
  }
316
248
  return loaded;
317
249
  }
318
-
319
250
  function renderDefaultConfig() {
320
251
  return `import { claudeAgentPlugin } from "@freestyle-sync/agent-claude";
321
252
  import { codexAgentPlugin } from "@freestyle-sync/agent-codex";
@@ -355,9 +286,8 @@ export default defineConfig({
355
286
  });
356
287
  `;
357
288
  }
358
-
359
- async function parseArgs(args: string[]): Promise<CliOptions> {
360
- const options: CliOptions = {
289
+ async function parseArgs(args) {
290
+ const options = {
361
291
  projectRoot: process.cwd(),
362
292
  cachePath: "",
363
293
  remoteProjectDir: "",
@@ -378,81 +308,97 @@ async function parseArgs(args: string[]): Promise<CliOptions> {
378
308
  autoSsh: true,
379
309
  envKeys: [],
380
310
  };
381
-
382
- const positional: string[] = [];
311
+ const positional = [];
383
312
  for (let index = 0; index < args.length; index += 1) {
384
313
  const arg = args[index];
385
314
  if (arg === "--help" || arg === "-h") {
386
315
  printHelp();
387
316
  process.exit(0);
388
- } else if (arg === "--yes" || arg === "-y") {
317
+ }
318
+ else if (arg === "--yes" || arg === "-y") {
389
319
  options.yes = true;
390
- } else if (arg === "--dry-run") {
320
+ }
321
+ else if (arg === "--dry-run") {
391
322
  options.dryRun = true;
392
- } else if (arg === "--disable-plugin") {
323
+ }
324
+ else if (arg === "--disable-plugin") {
393
325
  options.disablePlugins.push(readOptionValue(args, ++index, arg));
394
- } else if (arg === "--enable-plugin") {
326
+ }
327
+ else if (arg === "--enable-plugin") {
395
328
  options.enablePlugins.push(readOptionValue(args, ++index, arg));
396
- } else if (arg === "--reset-plugin-prefs" || arg === "--reset-context-prefs") {
329
+ }
330
+ else if (arg === "--reset-plugin-prefs" || arg === "--reset-context-prefs") {
397
331
  options.resetPluginPrefs = true;
398
- } else if (arg === "--list-plugins") {
332
+ }
333
+ else if (arg === "--list-plugins") {
399
334
  options.listPlugins = true;
400
- } else if (arg === "--no-auth") {
335
+ }
336
+ else if (arg === "--no-auth") {
401
337
  options.includeAuth = false;
402
- } else if (arg === "--no-agent-context") {
338
+ }
339
+ else if (arg === "--no-agent-context") {
403
340
  options.includeAgentContext = false;
404
- } else if (arg === "--no-git-dir") {
341
+ }
342
+ else if (arg === "--no-git-dir") {
405
343
  options.includeGitDir = false;
406
- } else if (arg === "--all-copilot-workspaces") {
344
+ }
345
+ else if (arg === "--all-copilot-workspaces") {
407
346
  options.includeAllCopilotWorkspaces = true;
408
- } else if (arg === "--no-snapshot" || arg === "--skip-snapshot") {
347
+ }
348
+ else if (arg === "--no-snapshot" || arg === "--skip-snapshot") {
409
349
  options.snapshot = false;
410
- } else if (arg === "--skip-sync") {
350
+ }
351
+ else if (arg === "--skip-sync") {
411
352
  options.skipSync = true;
412
- } else if (arg === "--install") {
353
+ }
354
+ else if (arg === "--install") {
413
355
  options.install = true;
414
- } else if (arg === "--no-ssh") {
356
+ }
357
+ else if (arg === "--no-ssh") {
415
358
  options.autoSsh = false;
416
- } else if (arg === "--vm-id") {
359
+ }
360
+ else if (arg === "--vm-id") {
417
361
  options.vmId = readOptionValue(args, ++index, arg);
418
- } else if (arg === "--name") {
362
+ }
363
+ else if (arg === "--name") {
419
364
  options.name = readOptionValue(args, ++index, arg);
420
- } else if (arg === "--remote-dir") {
365
+ }
366
+ else if (arg === "--remote-dir") {
421
367
  options.remoteProjectDir = normalizeRemotePath(readOptionValue(args, ++index, arg));
422
- } else if (arg === "--cache") {
368
+ }
369
+ else if (arg === "--cache") {
423
370
  options.cachePath = path.resolve(readOptionValue(args, ++index, arg));
424
- } else if (arg === "--idle-timeout") {
371
+ }
372
+ else if (arg === "--idle-timeout") {
425
373
  options.idleTimeoutSeconds = Number(readOptionValue(args, ++index, arg));
426
374
  if (!Number.isInteger(options.idleTimeoutSeconds) || options.idleTimeoutSeconds < 1) {
427
375
  throw new Error("--idle-timeout must be a positive integer");
428
376
  }
429
- } else if (arg === "--include-env") {
377
+ }
378
+ else if (arg === "--include-env") {
430
379
  options.envKeys.push(readOptionValue(args, ++index, arg));
431
- } else if (arg.startsWith("--")) {
380
+ }
381
+ else if (arg.startsWith("--")) {
432
382
  throw new Error(`unknown option: ${arg}`);
433
- } else {
383
+ }
384
+ else {
434
385
  positional.push(arg);
435
386
  }
436
387
  }
437
-
438
388
  if (positional.length > 1) {
439
389
  throw new Error("expected at most one project path");
440
390
  }
441
-
442
391
  options.projectRoot = path.resolve(positional[0] ?? options.projectRoot);
443
392
  const projectStats = await stat(options.projectRoot).catch(() => null);
444
393
  if (!projectStats?.isDirectory()) {
445
394
  throw new Error(`project path is not a directory: ${options.projectRoot}`);
446
395
  }
447
-
448
396
  const projectName = sanitizeName(path.basename(options.projectRoot));
449
397
  options.name ||= `vmpush-${projectName}`;
450
398
  options.remoteProjectDir ||= defaultRemoteProjectDir(options.projectRoot);
451
399
  options.cachePath ||= path.join(options.projectRoot, ".freestyle-sync", "cache.json");
452
-
453
400
  return options;
454
401
  }
455
-
456
402
  function printHelp() {
457
403
  console.log(`vmpush uploads the current project into a Freestyle VM.
458
404
 
@@ -485,18 +431,16 @@ Options:
485
431
  -h, --help Show this help.
486
432
  `);
487
433
  }
488
-
489
- function readOptionValue(args: string[], index: number, option: string) {
434
+ function readOptionValue(args, index, option) {
490
435
  const value = args[index];
491
436
  if (!value || value.startsWith("--")) {
492
437
  throw new Error(`${option} requires a value`);
493
438
  }
494
439
  return value;
495
440
  }
496
-
497
- async function readCache(cachePath: string, options: CliOptions): Promise<CacheFile> {
441
+ async function readCache(cachePath, options) {
498
442
  try {
499
- const parsed = JSON.parse(await readFile(cachePath, "utf8")) as CacheFile;
443
+ const parsed = JSON.parse(await readFile(cachePath, "utf8"));
500
444
  if (parsed.version === CACHE_VERSION && parsed.projectRoot === options.projectRoot && parsed.remoteProjectDir === options.remoteProjectDir) {
501
445
  return {
502
446
  ...parsed,
@@ -506,12 +450,12 @@ async function readCache(cachePath: string, options: CliOptions): Promise<CacheF
506
450
  snapshotContextFiles: parsed.snapshotContextFiles,
507
451
  };
508
452
  }
509
- } catch (error) {
510
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
453
+ }
454
+ catch (error) {
455
+ if (error.code !== "ENOENT") {
511
456
  console.warn(`Ignoring unreadable cache ${cachePath}: ${error instanceof Error ? error.message : String(error)}`);
512
457
  }
513
458
  }
514
-
515
459
  return {
516
460
  version: CACHE_VERSION,
517
461
  projectRoot: options.projectRoot,
@@ -520,8 +464,7 @@ async function readCache(cachePath: string, options: CliOptions): Promise<CacheF
520
464
  contextFiles: {},
521
465
  };
522
466
  }
523
-
524
- function cacheBaseForSync(options: CliOptions, cache: CacheFile): { projectFiles: Record<string, FileDigest>; contextFiles: Record<string, FileDigest>; envHash?: string } {
467
+ function cacheBaseForSync(options, cache) {
525
468
  if (options.vmId && options.vmId === cache.vmId) {
526
469
  return {
527
470
  projectFiles: cache.projectFiles,
@@ -529,7 +472,6 @@ function cacheBaseForSync(options: CliOptions, cache: CacheFile): { projectFiles
529
472
  envHash: cache.envHash,
530
473
  };
531
474
  }
532
-
533
475
  if (!options.vmId && cache.snapshotId) {
534
476
  return {
535
477
  projectFiles: cache.snapshotProjectFiles ?? {},
@@ -537,40 +479,30 @@ function cacheBaseForSync(options: CliOptions, cache: CacheFile): { projectFiles
537
479
  envHash: cache.snapshotEnvHash,
538
480
  };
539
481
  }
540
-
541
482
  return { projectFiles: {}, contextFiles: {} };
542
483
  }
543
-
544
- async function writeCache(cachePath: string, cache: CacheFile) {
484
+ async function writeCache(cachePath, cache) {
545
485
  await mkdir(path.dirname(cachePath), { recursive: true });
546
486
  await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
547
487
  }
548
-
549
- async function scanProject(projectRoot: string, includeGitDir: boolean): Promise<LocalEntry[]> {
550
- const entries: LocalEntry[] = [];
488
+ async function scanProject(projectRoot, includeGitDir) {
489
+ const entries = [];
551
490
  await walk(projectRoot, "", entries, {
552
491
  skipDirectory(relativePath, name) {
553
- if (!includeGitDir && relativePath === ".git") return true;
492
+ if (!includeGitDir && relativePath === ".git")
493
+ return true;
554
494
  return false;
555
495
  },
556
496
  });
557
497
  return entries.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
558
498
  }
559
-
560
- async function walk(
561
- root: string,
562
- relativePath: string,
563
- entries: LocalEntry[],
564
- options: { skipDirectory(relativePath: string, name: string): boolean },
565
- ) {
499
+ async function walk(root, relativePath, entries, options) {
566
500
  const absolutePath = path.join(root, relativePath);
567
501
  const dir = await import("node:fs/promises").then((fs) => fs.readdir(absolutePath, { withFileTypes: true }));
568
-
569
502
  for (const dirent of dir) {
570
503
  const childRelativePath = relativePath ? path.join(relativePath, dirent.name) : dirent.name;
571
504
  const normalizedRelativePath = toPosix(childRelativePath);
572
505
  const childAbsolutePath = path.join(root, childRelativePath);
573
-
574
506
  if (dirent.isDirectory()) {
575
507
  if (options.skipDirectory(normalizedRelativePath, dirent.name)) {
576
508
  continue;
@@ -578,14 +510,12 @@ async function walk(
578
510
  await walk(root, childRelativePath, entries, options);
579
511
  continue;
580
512
  }
581
-
582
513
  if (dirent.isFile() || dirent.isSymbolicLink()) {
583
514
  entries.push(await digestEntry(childAbsolutePath, normalizedRelativePath));
584
515
  }
585
516
  }
586
517
  }
587
-
588
- async function digestEntry(absolutePath: string, relativePath: string): Promise<LocalEntry> {
518
+ async function digestEntry(absolutePath, relativePath) {
589
519
  const stats = await import("node:fs/promises").then((fs) => fs.lstat(absolutePath));
590
520
  if (stats.isSymbolicLink()) {
591
521
  const target = await import("node:fs/promises").then((fs) => fs.readlink(absolutePath));
@@ -598,7 +528,6 @@ async function digestEntry(absolutePath: string, relativePath: string): Promise<
598
528
  hash: hashString(`symlink:${target}`),
599
529
  };
600
530
  }
601
-
602
531
  return {
603
532
  absolutePath,
604
533
  relativePath,
@@ -608,16 +537,13 @@ async function digestEntry(absolutePath: string, relativePath: string): Promise<
608
537
  hash: await hashFile(absolutePath),
609
538
  };
610
539
  }
611
-
612
- function digestMap(entries: LocalEntry[]): Record<string, FileDigest> {
540
+ function digestMap(entries) {
613
541
  return Object.fromEntries(entries.map((entry) => [entry.relativePath, stripLocal(entry)]));
614
542
  }
615
-
616
- function digestMapByRemotePath(entries: ContextEntry[]): Record<string, FileDigest> {
543
+ function digestMapByRemotePath(entries) {
617
544
  return Object.fromEntries(entries.map((entry) => [entry.remotePath, stripLocal(entry)]));
618
545
  }
619
-
620
- function stripLocal(entry: LocalEntry): FileDigest {
546
+ function stripLocal(entry) {
621
547
  return {
622
548
  hash: entry.hash,
623
549
  kind: entry.kind,
@@ -625,58 +551,52 @@ function stripLocal(entry: LocalEntry): FileDigest {
625
551
  size: entry.size,
626
552
  };
627
553
  }
628
-
629
- function diffEntries(entries: LocalEntry[], previous: Record<string, FileDigest>) {
554
+ function diffEntries(entries, previous) {
630
555
  const changed = entries.filter((entry) => previous[entry.relativePath]?.hash !== entry.hash);
631
556
  const currentKeys = new Set(entries.map((entry) => entry.relativePath));
632
557
  const removed = Object.keys(previous).filter((relativePath) => !currentKeys.has(relativePath));
633
558
  return { changed, removed, unchanged: entries.length - changed.length };
634
559
  }
635
-
636
- function diffContextEntries(entries: ContextEntry[], previous: Record<string, FileDigest>) {
560
+ function diffContextEntries(entries, previous) {
637
561
  const changed = entries.filter((entry) => previous[entry.remotePath]?.hash !== entry.hash);
638
562
  const currentKeys = new Set(entries.map((entry) => entry.remotePath));
639
563
  const removed = Object.keys(previous).filter((remotePath) => !currentKeys.has(remotePath) && !isProtectedRemotePath(remotePath));
640
564
  return { changed, removed, unchanged: entries.length - changed.length };
641
565
  }
642
-
643
- function collectPluginEnvironment(options: CliOptions) {
644
- const env: Record<string, string> = {};
566
+ function collectPluginEnvironment(options) {
567
+ const env = {};
645
568
  for (const plugin of plugins) {
646
569
  Object.assign(env, plugin.collectEnvironment?.({ options, utils: pluginUtils }) ?? {});
647
570
  }
648
571
  return env;
649
572
  }
650
-
651
- async function discoverPluginContextCandidates(options: CliOptions) {
652
- const candidates: ContextCandidate[] = [];
573
+ async function discoverPluginContextCandidates(options) {
574
+ const candidates = [];
653
575
  for (const plugin of plugins) {
654
576
  const discovered = await plugin.discoverContextCandidates?.({ options, utils: pluginUtils });
655
- if (discovered) candidates.push(...discovered);
577
+ if (discovered)
578
+ candidates.push(...discovered);
656
579
  }
657
580
  return dedupeContextCandidates(candidates);
658
581
  }
659
-
660
- function shouldSkipContextDirectory(relativePath: string, name: string) {
582
+ function shouldSkipContextDirectory(relativePath, name) {
661
583
  return plugins.some((plugin) => plugin.shouldSkipContextDirectory?.(relativePath, name));
662
584
  }
663
-
664
- function isProtectedRemotePath(remotePath: string) {
585
+ function isProtectedRemotePath(remotePath) {
665
586
  return plugins.some((plugin) => plugin.isProtectedRemotePath?.(remotePath));
666
587
  }
667
-
668
- function dedupeContextCandidates(candidates: ContextCandidate[]) {
669
- const seen = new Set<string>();
588
+ function dedupeContextCandidates(candidates) {
589
+ const seen = new Set();
670
590
  return candidates.filter((candidate) => {
671
591
  const key = `${candidate.source}\0${candidate.remoteRoot}`;
672
- if (seen.has(key)) return false;
592
+ if (seen.has(key))
593
+ return false;
673
594
  seen.add(key);
674
595
  return true;
675
596
  });
676
597
  }
677
-
678
- async function scanContextCandidates(candidates: ContextCandidate[]): Promise<ContextEntry[]> {
679
- const entries: ContextEntry[] = [];
598
+ async function scanContextCandidates(candidates) {
599
+ const entries = [];
680
600
  for (const candidate of candidates) {
681
601
  const stats = await import("node:fs/promises").then((fs) => fs.lstat(candidate.source));
682
602
  if (stats.isFile()) {
@@ -687,17 +607,20 @@ async function scanContextCandidates(candidates: ContextCandidate[]): Promise<Co
687
607
  label: candidate.label,
688
608
  sensitive: candidate.sensitive,
689
609
  });
690
- } else if (stats.isDirectory()) {
691
- const candidateEntries: LocalEntry[] = [];
610
+ }
611
+ else if (stats.isDirectory()) {
612
+ const candidateEntries = [];
692
613
  await walk(candidate.source, "", candidateEntries, {
693
614
  skipDirectory(relativePath, name) {
694
615
  return shouldSkipContextDirectory(relativePath, name);
695
616
  },
696
617
  });
697
618
  for (const local of candidateEntries) {
698
- if (local.kind === "symlink") continue;
619
+ if (local.kind === "symlink")
620
+ continue;
699
621
  const remotePath = `${candidate.remoteRoot}/${local.relativePath}`;
700
- if (isProtectedRemotePath(remotePath)) continue;
622
+ if (isProtectedRemotePath(remotePath))
623
+ continue;
701
624
  entries.push({
702
625
  ...local,
703
626
  remotePath,
@@ -709,24 +632,22 @@ async function scanContextCandidates(candidates: ContextCandidate[]): Promise<Co
709
632
  }
710
633
  return entries.sort((left, right) => left.remotePath.localeCompare(right.remotePath));
711
634
  }
712
-
713
- async function updatePluginPreferences(options: CliOptions): Promise<PluginPreferences> {
635
+ async function updatePluginPreferences(options) {
714
636
  const preferencesPath = getPluginPreferencesPath(options);
715
637
  const preferences = options.resetPluginPrefs ? emptyPluginPreferences() : await readPluginPreferences(preferencesPath);
716
638
  let changed = options.resetPluginPrefs;
717
- const disabledPlugins = new Set<string>();
718
-
639
+ const disabledPlugins = new Set();
719
640
  for (const savedName of preferences.disabledPlugins) {
720
641
  const canonicalName = maybeResolvePluginSelector(savedName);
721
642
  disabledPlugins.add(canonicalName ?? savedName);
722
- if (canonicalName && canonicalName !== savedName) changed = true;
643
+ if (canonicalName && canonicalName !== savedName)
644
+ changed = true;
723
645
  }
724
-
725
646
  for (const selector of options.enablePlugins) {
726
647
  const name = resolvePluginSelector(selector);
727
- if (disabledPlugins.delete(name)) changed = true;
648
+ if (disabledPlugins.delete(name))
649
+ changed = true;
728
650
  }
729
-
730
651
  for (const selector of options.disablePlugins) {
731
652
  const name = resolvePluginSelector(selector);
732
653
  if (!disabledPlugins.has(name)) {
@@ -734,23 +655,24 @@ async function updatePluginPreferences(options: CliOptions): Promise<PluginPrefe
734
655
  changed = true;
735
656
  }
736
657
  }
737
-
738
658
  const next = { version: PLUGIN_PREFERENCES_VERSION, disabledPlugins: [...disabledPlugins].sort(), updatedAt: preferences.updatedAt };
739
- if (changed) await writePluginPreferences(preferencesPath, next);
659
+ if (changed)
660
+ await writePluginPreferences(preferencesPath, next);
740
661
  return next;
741
662
  }
742
-
743
- function activePlugins(preferences: PluginPreferences, options: CliOptions) {
663
+ function activePlugins(preferences, options) {
744
664
  const disabled = new Set(preferences.disabledPlugins);
745
665
  return config.plugins.filter((plugin) => {
746
- if (disabled.has(plugin.name)) return false;
747
- if (!options.includeAuth && plugin.name.startsWith("@freestyle-sync/auth-")) return false;
748
- if (!options.includeAgentContext && plugin.name.startsWith("@freestyle-sync/agent-")) return false;
666
+ if (disabled.has(plugin.name))
667
+ return false;
668
+ if (!options.includeAuth && plugin.name.startsWith("@freestyle-sync/auth-"))
669
+ return false;
670
+ if (!options.includeAgentContext && plugin.name.startsWith("@freestyle-sync/agent-"))
671
+ return false;
749
672
  return true;
750
673
  });
751
674
  }
752
-
753
- function printPlugins(preferences: PluginPreferences, options: CliOptions) {
675
+ function printPlugins(preferences, options) {
754
676
  const enabled = new Set(activePlugins(preferences, options).map((plugin) => plugin.name));
755
677
  console.log(`Plugin preferences: ${getPluginPreferencesPath(options)}`);
756
678
  for (const plugin of config.plugins) {
@@ -760,48 +682,49 @@ function printPlugins(preferences: PluginPreferences, options: CliOptions) {
760
682
  console.log(`${status.padEnd(19)} ${plugin.name}`);
761
683
  }
762
684
  }
763
-
764
- function resolvePluginSelector(selector: string) {
685
+ function resolvePluginSelector(selector) {
765
686
  const match = maybeResolvePluginSelector(selector);
766
- if (match) return match;
687
+ if (match)
688
+ return match;
767
689
  const matches = config.plugins.filter((plugin) => pluginSelectorAliases(plugin.name).includes(selector));
768
- if (matches.length > 1) throw new Error(`ambiguous plugin selector ${selector}: ${matches.map((plugin) => plugin.name).join(", ")}`);
690
+ if (matches.length > 1)
691
+ throw new Error(`ambiguous plugin selector ${selector}: ${matches.map((plugin) => plugin.name).join(", ")}`);
769
692
  throw new Error(`unknown plugin ${selector}. Run --list-plugins to see configured plugins.`);
770
693
  }
771
-
772
- function maybeResolvePluginSelector(selector: string) {
694
+ function maybeResolvePluginSelector(selector) {
773
695
  const matches = config.plugins.filter((plugin) => pluginSelectorAliases(plugin.name).includes(selector));
774
- if (matches.length === 1) return matches[0].name;
775
- if (matches.length > 1) throw new Error(`ambiguous plugin selector ${selector}: ${matches.map((plugin) => plugin.name).join(", ")}`);
696
+ if (matches.length === 1)
697
+ return matches[0].name;
698
+ if (matches.length > 1)
699
+ throw new Error(`ambiguous plugin selector ${selector}: ${matches.map((plugin) => plugin.name).join(", ")}`);
776
700
  return undefined;
777
701
  }
778
-
779
- function pluginSelectorAliases(name: string) {
702
+ function pluginSelectorAliases(name) {
780
703
  const aliases = new Set([name]);
781
704
  const withoutScope = name.split("/").pop();
782
- if (withoutScope) aliases.add(withoutScope);
705
+ if (withoutScope)
706
+ aliases.add(withoutScope);
783
707
  if (name.startsWith("@freestyle-sync/") && withoutScope) {
784
708
  aliases.add(`@freestyle/sync-plugin-${withoutScope}`);
785
709
  aliases.add(`@pushvm/plugin-${withoutScope}`);
786
710
  aliases.add(`sync-plugin-${withoutScope}`);
787
711
  aliases.add(`plugin-${withoutScope}`);
788
712
  }
789
- if (withoutScope?.startsWith("sync-plugin-")) aliases.add(withoutScope.slice("sync-plugin-".length));
790
- if (withoutScope?.startsWith("plugin-")) aliases.add(withoutScope.slice("plugin-".length));
713
+ if (withoutScope?.startsWith("sync-plugin-"))
714
+ aliases.add(withoutScope.slice("sync-plugin-".length));
715
+ if (withoutScope?.startsWith("plugin-"))
716
+ aliases.add(withoutScope.slice("plugin-".length));
791
717
  return [...aliases];
792
718
  }
793
-
794
- function emptyPluginPreferences(): PluginPreferences {
719
+ function emptyPluginPreferences() {
795
720
  return { version: PLUGIN_PREFERENCES_VERSION, disabledPlugins: [] };
796
721
  }
797
-
798
- function getPluginPreferencesPath(options: CliOptions) {
722
+ function getPluginPreferencesPath(options) {
799
723
  return path.join(path.dirname(options.cachePath), "plugin-preferences.json");
800
724
  }
801
-
802
- async function readPluginPreferences(preferencesPath: string): Promise<PluginPreferences> {
725
+ async function readPluginPreferences(preferencesPath) {
803
726
  try {
804
- const parsed = JSON.parse(await readFile(preferencesPath, "utf8")) as PluginPreferences;
727
+ const parsed = JSON.parse(await readFile(preferencesPath, "utf8"));
805
728
  if (parsed.version === PLUGIN_PREFERENCES_VERSION) {
806
729
  return {
807
730
  version: PLUGIN_PREFERENCES_VERSION,
@@ -809,26 +732,19 @@ async function readPluginPreferences(preferencesPath: string): Promise<PluginPre
809
732
  updatedAt: parsed.updatedAt,
810
733
  };
811
734
  }
812
- } catch (error) {
813
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
735
+ }
736
+ catch (error) {
737
+ if (error.code !== "ENOENT") {
814
738
  console.warn(`Ignoring unreadable plugin preferences ${preferencesPath}: ${error instanceof Error ? error.message : String(error)}`);
815
739
  }
816
740
  }
817
741
  return emptyPluginPreferences();
818
742
  }
819
-
820
- async function writePluginPreferences(preferencesPath: string, preferences: PluginPreferences) {
743
+ async function writePluginPreferences(preferencesPath, preferences) {
821
744
  await mkdir(path.dirname(preferencesPath), { recursive: true });
822
745
  await writeFile(preferencesPath, `${JSON.stringify({ ...preferences, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
823
746
  }
824
-
825
- function printPlan(
826
- options: CliOptions,
827
- projectChanges: ReturnType<typeof diffEntries>,
828
- contextChanges: ReturnType<typeof diffContextEntries>,
829
- envExports: Record<string, string>,
830
- cache: CacheFile,
831
- ) {
747
+ function printPlan(options, projectChanges, contextChanges, envExports, cache) {
832
748
  const source = options.vmId ? `existing VM ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : "a new VM";
833
749
  console.log("");
834
750
  console.log(`${bold("Sync plan")}`);
@@ -843,56 +759,46 @@ function printPlan(
843
759
  }
844
760
  console.log("");
845
761
  }
846
-
847
- function totalEntrySize(entries: LocalEntry[]) {
762
+ function totalEntrySize(entries) {
848
763
  return entries.reduce((total, entry) => total + entry.size, 0);
849
764
  }
850
-
851
- function printHeading(name: string) {
765
+ function printHeading(name) {
852
766
  console.log(`${bold(name)} ${dim(`${symbol("→", "-")} Freestyle sync`)}`);
853
767
  console.log("");
854
768
  }
855
-
856
- function formatDuration(milliseconds: number) {
769
+ function formatDuration(milliseconds) {
857
770
  if (milliseconds < MS_PER_SECOND) {
858
771
  const rounded = Math.round(milliseconds);
859
772
  return `${rounded === 0 && milliseconds > 0 ? 1 : rounded}ms`;
860
773
  }
861
774
  return `${(milliseconds / MS_PER_SECOND).toFixed(1)}s`;
862
775
  }
863
-
864
- function symbol(unicode: string, ascii: string) {
776
+ function symbol(unicode, ascii) {
865
777
  return USE_UNICODE_OUTPUT ? unicode : ascii;
866
778
  }
867
-
868
- function accent(text: string) {
779
+ function accent(text) {
869
780
  return color(36, text);
870
781
  }
871
-
872
- function success(text: string) {
782
+ function success(text) {
873
783
  return color(32, text);
874
784
  }
875
-
876
- function dim(text: string) {
785
+ function dim(text) {
877
786
  return color(2, text);
878
787
  }
879
-
880
- function bold(text: string) {
788
+ function bold(text) {
881
789
  return color(1, text);
882
790
  }
883
-
884
- function color(code: number, text: string) {
885
- if (!USE_STYLED_OUTPUT) return text;
791
+ function color(code, text) {
792
+ if (!USE_STYLED_OUTPUT)
793
+ return text;
886
794
  return `\u001b[${code}m${text}\u001b[0m`;
887
795
  }
888
-
889
- async function getOrCreateVm(options: CliOptions, snapshotId?: string) {
796
+ async function getOrCreateVm(options, snapshotId) {
890
797
  if (options.vmId) {
891
798
  const { vm } = await freestyle.vms.get({ vmId: options.vmId });
892
799
  await vm.start(options.idleTimeoutSeconds ? { idleTimeoutSeconds: options.idleTimeoutSeconds } : undefined);
893
800
  return { vm, vmId: options.vmId };
894
801
  }
895
-
896
802
  console.log(snapshotId ? `Creating VM from snapshot ${snapshotId}...` : "Creating Freestyle VM...");
897
803
  const result = await freestyle.vms.create({
898
804
  name: options.name,
@@ -901,132 +807,100 @@ async function getOrCreateVm(options: CliOptions, snapshotId?: string) {
901
807
  });
902
808
  return { vm: result.vm, vmId: result.vmId };
903
809
  }
904
-
905
- async function ensureRemoteBase(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], remoteProjectDir: string) {
810
+ async function ensureRemoteBase(vm, remoteProjectDir) {
906
811
  await checkedExec(vm, `mkdir -p ${shellQuote(remoteProjectDir)} /root/.freestyle-sync`);
907
812
  }
908
-
909
- async function syncProject(
910
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
911
- vmId: string,
912
- options: CliOptions,
913
- changes: ReturnType<typeof diffEntries>,
914
- ): Promise<SyncResult> {
813
+ async function syncProject(vm, vmId, options, changes) {
915
814
  if (changes.changed.length > 0) {
916
815
  console.log(`VM ${vmId}: uploading ${changes.changed.length} changed project files...`);
917
816
  const archive = await createProjectArchive(options.projectRoot, changes.changed);
918
817
  try {
919
818
  await uploadArchiveInChunks(vm, vmId, archive, "/tmp/vmpush-project.tgz", "project");
920
819
  await checkedExec(vm, `mkdir -p ${shellQuote(options.remoteProjectDir)} && tar --no-same-owner --no-same-permissions -xzf /tmp/vmpush-project.tgz -C ${shellQuote(options.remoteProjectDir)} && rm -f /tmp/vmpush-project.tgz`);
921
- } finally {
820
+ }
821
+ finally {
922
822
  await rm(path.dirname(archive), { recursive: true, force: true });
923
823
  }
924
824
  }
925
-
926
825
  if (changes.removed.length > 0) {
927
826
  console.log(`VM ${vmId}: removing ${changes.removed.length} deleted project files...`);
928
827
  const removeList = Buffer.from(changes.removed.join("\0") + "\0");
929
828
  await vm.fs.writeFile("/tmp/vmpush-remove-list", removeList);
930
829
  await checkedExec(vm, `cd ${shellQuote(options.remoteProjectDir)} && xargs -0 rm -f -- < /tmp/vmpush-remove-list && rm -f /tmp/vmpush-remove-list`);
931
830
  }
932
-
933
831
  return {
934
832
  uploaded: changes.changed.length,
935
833
  removed: changes.removed.length,
936
834
  unchanged: changes.unchanged,
937
835
  };
938
836
  }
939
-
940
- async function runRemoteFixups(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], vmId: string, options: CliOptions) {
837
+ async function runRemoteFixups(vm, vmId, options) {
941
838
  for (const plugin of plugins) {
942
- if (!plugin.afterProjectSync) continue;
839
+ if (!plugin.afterProjectSync)
840
+ continue;
943
841
  try {
944
- await plugin.afterProjectSync({ vm: vm as RemoteVm, vmId, options, utils: pluginUtils });
945
- } catch (error) {
842
+ await plugin.afterProjectSync({ vm: vm, vmId, options, utils: pluginUtils });
843
+ }
844
+ catch (error) {
946
845
  console.warn(`Remote fixup ${plugin.name} skipped: ${error instanceof Error ? error.message : String(error)}`);
947
846
  }
948
847
  }
949
848
  }
950
-
951
- async function runAfterContextSyncPlugins(
952
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
953
- vmId: string,
954
- options: CliOptions,
955
- changedRemotePaths: string[],
956
- ) {
849
+ async function runAfterContextSyncPlugins(vm, vmId, options, changedRemotePaths) {
957
850
  for (const plugin of plugins) {
958
- if (!plugin.afterContextSync) continue;
959
- await plugin.afterContextSync({ vm: vm as RemoteVm, vmId, options, utils: pluginUtils, changedRemotePaths });
851
+ if (!plugin.afterContextSync)
852
+ continue;
853
+ await plugin.afterContextSync({ vm: vm, vmId, options, utils: pluginUtils, changedRemotePaths });
960
854
  }
961
855
  }
962
-
963
- async function runAfterSyncPlugins(
964
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
965
- vmId: string,
966
- options: CliOptions,
967
- contextCandidates: ContextCandidate[],
968
- ) {
969
- const messages: string[] = [];
856
+ async function runAfterSyncPlugins(vm, vmId, options, contextCandidates) {
857
+ const messages = [];
970
858
  for (const plugin of plugins) {
971
- const result = await plugin.afterSync?.({ vm: vm as RemoteVm, vmId, options, utils: pluginUtils, contextCandidates });
972
- if (result) messages.push(...result);
859
+ const result = await plugin.afterSync?.({ vm: vm, vmId, options, utils: pluginUtils, contextCandidates });
860
+ if (result)
861
+ messages.push(...result);
973
862
  }
974
863
  return messages;
975
864
  }
976
-
977
- async function runBeforeSnapshotPlugins(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], vmId: string, options: CliOptions) {
865
+ async function runBeforeSnapshotPlugins(vm, vmId, options) {
978
866
  for (const plugin of plugins) {
979
- if (!plugin.beforeSnapshot) continue;
867
+ if (!plugin.beforeSnapshot)
868
+ continue;
980
869
  try {
981
- await plugin.beforeSnapshot({ vm: vm as RemoteVm, vmId, options, utils: pluginUtils });
982
- } catch (error) {
870
+ await plugin.beforeSnapshot({ vm: vm, vmId, options, utils: pluginUtils });
871
+ }
872
+ catch (error) {
983
873
  console.warn(`Snapshot preparation ${plugin.name} skipped: ${error instanceof Error ? error.message : String(error)}`);
984
874
  }
985
875
  }
986
876
  }
987
-
988
- async function syncContext(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], vmId: string, changes: ReturnType<typeof diffContextEntries>): Promise<SyncResult> {
877
+ async function syncContext(vm, vmId, changes) {
989
878
  if (changes.removed.length > 0) {
990
879
  console.log(`VM ${vmId}: removing ${changes.removed.length} deleted auth/context files...`);
991
880
  await checkedExec(vm, changes.removed.map((remotePath) => `rm -f -- ${shellQuote(remotePath)}`).join("\n"));
992
881
  }
993
-
994
882
  if (changes.changed.length === 0) {
995
883
  return { uploaded: 0, removed: changes.removed.length, unchanged: changes.unchanged };
996
884
  }
997
-
998
885
  console.log(`VM ${vmId}: uploading ${changes.changed.length} changed auth/context files...`);
999
886
  const archive = await createContextArchive(changes.changed);
1000
887
  try {
1001
888
  await uploadArchiveInChunks(vm, vmId, archive, "/tmp/vmpush-context.tgz", "context");
1002
889
  await checkedExec(vm, "tar --no-same-owner --no-same-permissions -xzf /tmp/vmpush-context.tgz -C / && rm -f /tmp/vmpush-context.tgz");
1003
- } finally {
890
+ }
891
+ finally {
1004
892
  await rm(path.dirname(archive), { recursive: true, force: true });
1005
893
  }
1006
894
  return { uploaded: changes.changed.length, removed: changes.removed.length, unchanged: changes.unchanged };
1007
895
  }
1008
-
1009
- async function installEnvironment(
1010
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
1011
- envExports: Record<string, string>,
1012
- envHash: string,
1013
- previousHash?: string,
1014
- ) {
1015
- if (Object.keys(envExports).length === 0 || envHash === previousHash) return;
896
+ async function installEnvironment(vm, envExports, envHash, previousHash) {
897
+ if (Object.keys(envExports).length === 0 || envHash === previousHash)
898
+ return;
1016
899
  await checkedExec(vm, "mkdir -p /root/.freestyle-sync");
1017
900
  await vm.fs.writeTextFile("/root/.freestyle-sync/env.sh", renderEnvFile(envExports));
1018
- await checkedExec(
1019
- vm,
1020
- "chmod 700 /root/.freestyle-sync && chmod 600 /root/.freestyle-sync/env.sh && grep -qxF 'test -f /root/.freestyle-sync/env.sh && . /root/.freestyle-sync/env.sh' /root/.profile || printf '\n%s\n' 'test -f /root/.freestyle-sync/env.sh && . /root/.freestyle-sync/env.sh' >> /root/.profile",
1021
- );
901
+ await checkedExec(vm, "chmod 700 /root/.freestyle-sync && chmod 600 /root/.freestyle-sync/env.sh && grep -qxF 'test -f /root/.freestyle-sync/env.sh && . /root/.freestyle-sync/env.sh' /root/.profile || printf '\n%s\n' 'test -f /root/.freestyle-sync/env.sh && . /root/.freestyle-sync/env.sh' >> /root/.profile");
1022
902
  }
1023
-
1024
- async function writeRemoteResumeFiles(
1025
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
1026
- options: CliOptions,
1027
- vmId: string,
1028
- envExports: Record<string, string>,
1029
- ) {
903
+ async function writeRemoteResumeFiles(vm, options, vmId, envExports) {
1030
904
  const manifest = {
1031
905
  vmId,
1032
906
  project: options.remoteProjectDir,
@@ -1036,23 +910,16 @@ async function writeRemoteResumeFiles(
1036
910
  };
1037
911
  await checkedExec(vm, `mkdir -p ${shellQuote(`${options.remoteProjectDir}/.freestyle-sync`)} /root/.freestyle-sync`);
1038
912
  await vm.fs.writeTextFile(`${options.remoteProjectDir}/.freestyle-sync/remote.json`, `${JSON.stringify(manifest, null, 2)}\n`);
1039
- await vm.fs.writeTextFile(
1040
- "/root/.freestyle-sync/resume.sh",
1041
- `#!/usr/bin/env bash
913
+ await vm.fs.writeTextFile("/root/.freestyle-sync/resume.sh", `#!/usr/bin/env bash
1042
914
  set -euo pipefail
1043
915
  test -f /root/.freestyle-sync/env.sh && . /root/.freestyle-sync/env.sh
1044
916
  cd ${shellQuote(options.remoteProjectDir)}
1045
917
  exec "$SHELL" -l
1046
- `,
1047
- );
918
+ `);
1048
919
  await vm.fs.writeTextFile("/root/.freestyle-sync/profile.sh", renderRemoteProfile(options.remoteProjectDir));
1049
- await checkedExec(
1050
- vm,
1051
- "chmod 700 /root/.freestyle-sync && chmod +x /root/.freestyle-sync/resume.sh && chmod 600 /root/.freestyle-sync/profile.sh && grep -qxF 'test -f /root/.freestyle-sync/profile.sh && . /root/.freestyle-sync/profile.sh' /root/.profile || printf '\n%s\n' 'test -f /root/.freestyle-sync/profile.sh && . /root/.freestyle-sync/profile.sh' >> /root/.profile",
1052
- );
920
+ await checkedExec(vm, "chmod 700 /root/.freestyle-sync && chmod +x /root/.freestyle-sync/resume.sh && chmod 600 /root/.freestyle-sync/profile.sh && grep -qxF 'test -f /root/.freestyle-sync/profile.sh && . /root/.freestyle-sync/profile.sh' /root/.profile || printf '\n%s\n' 'test -f /root/.freestyle-sync/profile.sh && . /root/.freestyle-sync/profile.sh' >> /root/.profile");
1053
921
  }
1054
-
1055
- function renderRemoteProfile(remoteProjectDir: string) {
922
+ function renderRemoteProfile(remoteProjectDir) {
1056
923
  return `# Generated by freestyle-sync. Loaded by /root/.profile.
1057
924
  test -f /root/.freestyle-sync/env.sh && . /root/.freestyle-sync/env.sh
1058
925
  if [ -d ${shellQuote(remoteProjectDir)} ]; then
@@ -1060,8 +927,7 @@ if [ -d ${shellQuote(remoteProjectDir)} ]; then
1060
927
  fi
1061
928
  `;
1062
929
  }
1063
-
1064
- async function runInstall(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], projectRoot: string, remoteProjectDir: string) {
930
+ async function runInstall(vm, projectRoot, remoteProjectDir) {
1065
931
  const installCommand = await detectInstallCommand(projectRoot);
1066
932
  if (!installCommand) {
1067
933
  console.log("No dependency install command detected.");
@@ -1070,19 +936,24 @@ async function runInstall(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], p
1070
936
  console.log(`Running install: ${installCommand}`);
1071
937
  await checkedExec(vm, `cd ${shellQuote(remoteProjectDir)} && ${installCommand}`, 20 * 60 * 1000);
1072
938
  }
1073
-
1074
- async function detectInstallCommand(projectRoot: string) {
1075
- if (await exists(path.join(projectRoot, "pnpm-lock.yaml"))) return "corepack enable && pnpm install";
1076
- if (await exists(path.join(projectRoot, "yarn.lock"))) return "corepack enable && yarn install";
1077
- if (await exists(path.join(projectRoot, "package-lock.json"))) return "npm install";
1078
- if (await exists(path.join(projectRoot, "requirements.txt"))) return "python3 -m pip install -r requirements.txt";
1079
- if (await exists(path.join(projectRoot, "pyproject.toml"))) return "python3 -m pip install -e .";
1080
- if (await exists(path.join(projectRoot, "Cargo.toml"))) return "cargo fetch";
1081
- if (await exists(path.join(projectRoot, "go.mod"))) return "go mod download";
939
+ async function detectInstallCommand(projectRoot) {
940
+ if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
941
+ return "corepack enable && pnpm install";
942
+ if (await exists(path.join(projectRoot, "yarn.lock")))
943
+ return "corepack enable && yarn install";
944
+ if (await exists(path.join(projectRoot, "package-lock.json")))
945
+ return "npm install";
946
+ if (await exists(path.join(projectRoot, "requirements.txt")))
947
+ return "python3 -m pip install -r requirements.txt";
948
+ if (await exists(path.join(projectRoot, "pyproject.toml")))
949
+ return "python3 -m pip install -e .";
950
+ if (await exists(path.join(projectRoot, "Cargo.toml")))
951
+ return "cargo fetch";
952
+ if (await exists(path.join(projectRoot, "go.mod")))
953
+ return "go mod download";
1082
954
  return undefined;
1083
955
  }
1084
-
1085
- async function createProjectArchive(projectRoot: string, entries: LocalEntry[]) {
956
+ async function createProjectArchive(projectRoot, entries) {
1086
957
  const tempDir = await mkdtemp(path.join(tmpdir(), "vmpush-"));
1087
958
  const listPath = path.join(tempDir, "files.list");
1088
959
  const archivePath = path.join(tempDir, "project.tgz");
@@ -1090,25 +961,21 @@ async function createProjectArchive(projectRoot: string, entries: LocalEntry[])
1090
961
  await createTar(["--null", "--no-xattrs", "-T", listPath, "-czf", archivePath, "-C", projectRoot]);
1091
962
  return archivePath;
1092
963
  }
1093
-
1094
- async function createContextArchive(entries: ContextEntry[]) {
964
+ async function createContextArchive(entries) {
1095
965
  const tempDir = await mkdtemp(path.join(tmpdir(), "vmpush-context-"));
1096
966
  const stagingDir = path.join(tempDir, "staging");
1097
967
  const archivePath = path.join(tempDir, "context.tgz");
1098
968
  await mkdir(stagingDir, { recursive: true });
1099
-
1100
969
  for (const entry of entries) {
1101
970
  const relativeRemotePath = entry.remotePath.replace(/^\/+/, "");
1102
971
  const destination = path.join(stagingDir, ...relativeRemotePath.split("/"));
1103
972
  await mkdir(path.dirname(destination), { recursive: true });
1104
973
  await writeFile(destination, await readFile(entry.absolutePath));
1105
974
  }
1106
-
1107
975
  await createTar(["--no-xattrs", "-czf", archivePath, "-C", stagingDir, "."]);
1108
976
  return archivePath;
1109
977
  }
1110
-
1111
- async function createTar(args: string[]) {
978
+ async function createTar(args) {
1112
979
  await execFileAsync("tar", args, {
1113
980
  env: {
1114
981
  ...process.env,
@@ -1116,22 +983,13 @@ async function createTar(args: string[]) {
1116
983
  },
1117
984
  });
1118
985
  }
1119
-
1120
- async function uploadArchiveInChunks(
1121
- vm: RemoteVm,
1122
- vmId: string,
1123
- archivePath: string,
1124
- remoteArchivePath: string,
1125
- label: string,
1126
- ) {
986
+ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, label) {
1127
987
  const archive = await readFile(archivePath);
1128
988
  const encoded = archive.toString("base64");
1129
989
  const chunkCount = Math.max(1, Math.ceil(encoded.length / ARCHIVE_CHUNK_CHARS));
1130
990
  const chunkDir = `/tmp/vmpush-${label}-${Date.now()}.chunks`;
1131
-
1132
991
  console.log(`VM ${vmId}: streaming ${formatBytes(archive.length)} ${label} archive in ${chunkCount} chunk${chunkCount === 1 ? "" : "s"}...`);
1133
992
  await checkedExec(vm, `rm -rf ${shellQuote(chunkDir)} && mkdir -p ${shellQuote(chunkDir)}`);
1134
-
1135
993
  const width = String(chunkCount - 1).length;
1136
994
  for (let index = 0; index < chunkCount; index += 1) {
1137
995
  const start = index * ARCHIVE_CHUNK_CHARS;
@@ -1142,43 +1000,28 @@ async function uploadArchiveInChunks(
1142
1000
  console.log(`VM ${vmId}: uploaded ${index + 1}/${chunkCount} ${label} archive chunks`);
1143
1001
  }
1144
1002
  }
1145
-
1146
- await checkedExec(
1147
- vm,
1148
- `cat ${shellQuote(chunkDir)}/*.b64 | base64 -d > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`,
1149
- );
1003
+ await checkedExec(vm, `cat ${shellQuote(chunkDir)}/*.b64 | base64 -d > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`);
1150
1004
  }
1151
-
1152
- async function mkdirRemote(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"], directories: string[]) {
1005
+ async function mkdirRemote(vm, directories) {
1153
1006
  for (const chunk of chunkArray(directories, 50)) {
1154
1007
  await checkedExec(vm, `mkdir -p ${chunk.map(shellQuote).join(" ")}`);
1155
1008
  }
1156
1009
  }
1157
-
1158
- async function hardenRemoteRoot(vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"]): Promise<void> {
1010
+ async function hardenRemoteRoot(vm) {
1159
1011
  await checkedExec(vm, "chown root:root /root && chmod 700 /root");
1160
1012
  }
1161
-
1162
- async function checkedExec(vm: RemoteVm, command: string, timeoutMs?: number) {
1013
+ async function checkedExec(vm, command, timeoutMs) {
1163
1014
  const result = await vm.exec({ command, timeoutMs });
1164
1015
  if (result.statusCode && result.statusCode !== 0) {
1165
1016
  throw new Error(`remote command failed (${result.statusCode}): ${command}\n${result.stderr ?? result.stdout ?? ""}`);
1166
1017
  }
1167
1018
  return result;
1168
1019
  }
1169
-
1170
- async function runBeforeOpenRemoteEditorPlugins(
1171
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
1172
- vmId: string,
1173
- options: CliOptions,
1174
- scheme: string,
1175
- remoteWorkspaceUri: string,
1176
- contextCandidates: ContextCandidate[],
1177
- ) {
1178
- const messages: string[] = [];
1020
+ async function runBeforeOpenRemoteEditorPlugins(vm, vmId, options, scheme, remoteWorkspaceUri, contextCandidates) {
1021
+ const messages = [];
1179
1022
  for (const plugin of plugins) {
1180
1023
  const result = await plugin.beforeOpenRemoteEditor?.({
1181
- vm: vm as RemoteVm,
1024
+ vm: vm,
1182
1025
  vmId,
1183
1026
  options,
1184
1027
  utils: pluginUtils,
@@ -1186,23 +1029,16 @@ async function runBeforeOpenRemoteEditorPlugins(
1186
1029
  remoteWorkspaceUri,
1187
1030
  contextCandidates,
1188
1031
  });
1189
- if (result) messages.push(...result);
1032
+ if (result)
1033
+ messages.push(...result);
1190
1034
  }
1191
1035
  return messages;
1192
1036
  }
1193
-
1194
- async function runAfterOpenRemoteEditorPlugins(
1195
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
1196
- vmId: string,
1197
- options: CliOptions,
1198
- scheme: string,
1199
- remoteWorkspaceUri: string,
1200
- contextCandidates: ContextCandidate[],
1201
- ) {
1202
- const messages: string[] = [];
1037
+ async function runAfterOpenRemoteEditorPlugins(vm, vmId, options, scheme, remoteWorkspaceUri, contextCandidates) {
1038
+ const messages = [];
1203
1039
  for (const plugin of plugins) {
1204
1040
  const result = await plugin.afterOpenRemoteEditor?.({
1205
- vm: vm as RemoteVm,
1041
+ vm: vm,
1206
1042
  vmId,
1207
1043
  options,
1208
1044
  utils: pluginUtils,
@@ -1210,20 +1046,15 @@ async function runAfterOpenRemoteEditorPlugins(
1210
1046
  remoteWorkspaceUri,
1211
1047
  contextCandidates,
1212
1048
  });
1213
- if (result) messages.push(...result);
1049
+ if (result)
1050
+ messages.push(...result);
1214
1051
  }
1215
1052
  return messages;
1216
1053
  }
1217
-
1218
- async function runConnectPlugins(
1219
- vm: Awaited<ReturnType<typeof getOrCreateVm>>["vm"],
1220
- vmId: string,
1221
- options: CliOptions,
1222
- contextCandidates: ContextCandidate[],
1223
- ) {
1054
+ async function runConnectPlugins(vm, vmId, options, contextCandidates) {
1224
1055
  for (const plugin of plugins) {
1225
1056
  const handled = await plugin.connect?.({
1226
- vm: vm as RemoteVm,
1057
+ vm: vm,
1227
1058
  vmId,
1228
1059
  options,
1229
1060
  utils: pluginUtils,
@@ -1231,14 +1062,14 @@ async function runConnectPlugins(
1231
1062
  runBeforeOpenRemoteEditor: ({ scheme, remoteWorkspaceUri }) => runBeforeOpenRemoteEditorPlugins(vm, vmId, options, scheme, remoteWorkspaceUri, contextCandidates),
1232
1063
  runAfterOpenRemoteEditor: ({ scheme, remoteWorkspaceUri }) => runAfterOpenRemoteEditorPlugins(vm, vmId, options, scheme, remoteWorkspaceUri, contextCandidates),
1233
1064
  });
1234
- if (handled) return true;
1065
+ if (handled)
1066
+ return true;
1235
1067
  }
1236
1068
  return false;
1237
1069
  }
1238
-
1239
- async function sshIntoVm(vmId: string) {
1070
+ async function sshIntoVm(vmId) {
1240
1071
  console.log(`Connecting to VM ${vmId}...`);
1241
- const exitCode = await new Promise<number | null>((resolve, reject) => {
1072
+ const exitCode = await new Promise((resolve, reject) => {
1242
1073
  const child = spawn("npx", ["freestyle", "vm", "ssh", vmId], { stdio: "inherit" });
1243
1074
  child.on("error", reject);
1244
1075
  child.on("exit", (code) => resolve(code));
@@ -1247,10 +1078,9 @@ async function sshIntoVm(vmId: string) {
1247
1078
  throw new Error(`ssh exited with status ${exitCode}`);
1248
1079
  }
1249
1080
  }
1250
-
1251
- async function hashFile(filePath: string): Promise<string> {
1081
+ async function hashFile(filePath) {
1252
1082
  const hash = createHash("sha256");
1253
- await new Promise<void>((resolve, reject) => {
1083
+ await new Promise((resolve, reject) => {
1254
1084
  const stream = createReadStream(filePath);
1255
1085
  stream.on("data", (chunk) => hash.update(chunk));
1256
1086
  stream.on("error", reject);
@@ -1258,68 +1088,59 @@ async function hashFile(filePath: string): Promise<string> {
1258
1088
  });
1259
1089
  return hash.digest("hex");
1260
1090
  }
1261
-
1262
- function hashString(value: string): string {
1091
+ function hashString(value) {
1263
1092
  return createHash("sha256").update(value).digest("hex");
1264
1093
  }
1265
-
1266
- function md5(value: string): string {
1094
+ function md5(value) {
1267
1095
  return createHash("md5").update(value).digest("hex");
1268
1096
  }
1269
-
1270
- function renderEnvFile(envExports: Record<string, string>) {
1097
+ function renderEnvFile(envExports) {
1271
1098
  const lines = ["# Generated by vmpush. Contains local auth tokens."];
1272
1099
  for (const key of Object.keys(envExports).sort()) {
1273
1100
  lines.push(`export ${key}=${shellQuote(envExports[key])}`);
1274
1101
  }
1275
1102
  return `${lines.join("\n")}\n`;
1276
1103
  }
1277
-
1278
- function shellQuote(value: string): string {
1104
+ function shellQuote(value) {
1279
1105
  return `'${value.replace(/'/g, `'"'"'`)}'`;
1280
1106
  }
1281
-
1282
- function normalizeRemotePath(value: string): string {
1107
+ function normalizeRemotePath(value) {
1283
1108
  const normalized = path.posix.normalize(value.replace(/\\/g, "/"));
1284
1109
  if (!normalized.startsWith("/")) {
1285
1110
  throw new Error("--remote-dir must be an absolute VM path");
1286
1111
  }
1287
1112
  return normalized;
1288
1113
  }
1289
-
1290
- function defaultRemoteProjectDir(projectRoot: string): string {
1114
+ function defaultRemoteProjectDir(projectRoot) {
1291
1115
  return normalizeRemotePath(toPosix(projectRoot));
1292
1116
  }
1293
-
1294
- function sanitizeName(value: string): string {
1117
+ function sanitizeName(value) {
1295
1118
  return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
1296
1119
  }
1297
-
1298
- function toPosix(value: string) {
1120
+ function toPosix(value) {
1299
1121
  return value.split(path.sep).join(path.posix.sep);
1300
1122
  }
1301
-
1302
- function formatBytes(bytes: number) {
1303
- if (bytes < 1024) return `${bytes} B`;
1304
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1123
+ function formatBytes(bytes) {
1124
+ if (bytes < 1024)
1125
+ return `${bytes} B`;
1126
+ if (bytes < 1024 * 1024)
1127
+ return `${(bytes / 1024).toFixed(1)} KB`;
1305
1128
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1306
1129
  }
1307
-
1308
- async function delay(ms: number) {
1130
+ async function delay(ms) {
1309
1131
  await new Promise((resolve) => setTimeout(resolve, ms));
1310
1132
  }
1311
-
1312
- async function exists(filePath: string) {
1133
+ async function exists(filePath) {
1313
1134
  try {
1314
1135
  await stat(filePath);
1315
1136
  return true;
1316
- } catch {
1137
+ }
1138
+ catch {
1317
1139
  return false;
1318
1140
  }
1319
1141
  }
1320
-
1321
- function chunkArray<T>(items: T[], size: number): T[][] {
1322
- const chunks: T[][] = [];
1142
+ function chunkArray(items, size) {
1143
+ const chunks = [];
1323
1144
  for (let index = 0; index < items.length; index += size) {
1324
1145
  chunks.push(items.slice(index, index + size));
1325
1146
  }