freestyle-sync 0.1.4 → 0.1.5
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 +28 -2
- package/dist/src/main.js +317 -72
- package/package.json +17 -4
- package/dist/freestyle-sync.config.js +0 -35
- package/dist/main.js +0 -1319
- package/dist/plugins/agent-claude/src/index.js +0 -116
- package/dist/plugins/agent-codex/src/index.js +0 -68
- package/dist/plugins/agent-copilot/src/index.js +0 -529
- package/dist/plugins/auth-aws/src/index.js +0 -29
- package/dist/plugins/auth-azure/src/index.js +0 -29
- package/dist/plugins/auth-context.js +0 -213
- package/dist/plugins/auth-docker/src/index.js +0 -29
- package/dist/plugins/auth-env/src/index.js +0 -35
- package/dist/plugins/auth-gcloud/src/index.js +0 -29
- package/dist/plugins/auth-git/src/index.js +0 -50
- package/dist/plugins/auth-github-cli/src/index.js +0 -39
- package/dist/plugins/auth-npm/src/index.js +0 -38
- package/dist/plugins/auth-ssh/src/index.js +0 -42
- package/dist/plugins/auth-yarn/src/index.js +0 -24
- package/dist/plugins/node-npm/src/index.js +0 -388
- package/dist/plugins/npm-native-deps.js +0 -307
- package/dist/plugins/shell-history/src/index.js +0 -65
- package/dist/plugins/vscode/src/index.js +0 -160
- package/dist/src/pushvm.config.js +0 -36
package/README.md
CHANGED
|
@@ -6,8 +6,35 @@ Sync your current directory, it's dependencies, and your agent context into a VM
|
|
|
6
6
|
npx freestyle-sync
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Use the SDK when embedding sync inside another CLI/app (no required `freestyle-sync.config.ts` or cache file):
|
|
10
|
+
|
|
10
11
|
```ts
|
|
12
|
+
import { defineConfig, sync, type SyncCache } from "freestyle-sync";
|
|
13
|
+
import { gitAuthPlugin } from "@freestyle-sync/auth-git";
|
|
14
|
+
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
15
|
+
|
|
16
|
+
let cache: SyncCache | undefined;
|
|
17
|
+
|
|
18
|
+
const result = await sync({
|
|
19
|
+
config: defineConfig({
|
|
20
|
+
plugins: [gitAuthPlugin(), nodeNpmPlugin()],
|
|
21
|
+
}),
|
|
22
|
+
options: {
|
|
23
|
+
projectRoot: process.cwd(),
|
|
24
|
+
autoSsh: false,
|
|
25
|
+
},
|
|
26
|
+
cache,
|
|
27
|
+
onCacheUpdate(nextCache) {
|
|
28
|
+
cache = nextCache;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log("synced VM", result.vmId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
```js
|
|
11
38
|
import { defineConfig } from "freestyle-sync";
|
|
12
39
|
import { claudeAgentPlugin } from "@freestyle-sync/agent-claude";
|
|
13
40
|
import { codexAgentPlugin } from "@freestyle-sync/agent-codex";
|
|
@@ -44,5 +71,4 @@ export default defineConfig({
|
|
|
44
71
|
shellHistoryPlugin(),
|
|
45
72
|
],
|
|
46
73
|
});
|
|
47
|
-
|
|
48
74
|
```
|
package/dist/src/main.js
CHANGED
|
@@ -19,14 +19,31 @@ import { promisify } from "node:util";
|
|
|
19
19
|
import { freestyle } from "freestyle";
|
|
20
20
|
export * from "./plugin-api.js";
|
|
21
21
|
const execFileAsync = promisify(execFile);
|
|
22
|
+
const CLI_NAME = "freestyle-sync";
|
|
22
23
|
const CACHE_VERSION = 1;
|
|
23
24
|
const PLUGIN_PREFERENCES_VERSION = 1;
|
|
24
25
|
const ARCHIVE_CHUNK_CHARS = 1024 * 1024;
|
|
25
26
|
const MS_PER_SECOND = 1000;
|
|
26
27
|
const USE_UNICODE_OUTPUT = process.stdout.isTTY && (process.env.TERM !== "dumb" || Boolean(process.env.TERM_PROGRAM));
|
|
27
28
|
const USE_STYLED_OUTPUT = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const DEFAULT_CONFIG_DEPENDENCIES = [
|
|
30
|
+
{ name: "freestyle-sync", spec: "freestyle-sync@latest" },
|
|
31
|
+
{ name: "@freestyle-sync/agent-claude", spec: "@freestyle-sync/agent-claude@latest" },
|
|
32
|
+
{ name: "@freestyle-sync/agent-codex", spec: "@freestyle-sync/agent-codex@latest" },
|
|
33
|
+
{ name: "@freestyle-sync/agent-copilot", spec: "@freestyle-sync/agent-copilot@latest" },
|
|
34
|
+
{ name: "@freestyle-sync/auth-aws", spec: "@freestyle-sync/auth-aws@latest" },
|
|
35
|
+
{ name: "@freestyle-sync/auth-azure", spec: "@freestyle-sync/auth-azure@latest" },
|
|
36
|
+
{ name: "@freestyle-sync/auth-docker", spec: "@freestyle-sync/auth-docker@latest" },
|
|
37
|
+
{ name: "@freestyle-sync/auth-env", spec: "@freestyle-sync/auth-env@latest" },
|
|
38
|
+
{ name: "@freestyle-sync/auth-gcloud", spec: "@freestyle-sync/auth-gcloud@latest" },
|
|
39
|
+
{ name: "@freestyle-sync/auth-git", spec: "@freestyle-sync/auth-git@latest" },
|
|
40
|
+
{ name: "@freestyle-sync/auth-github-cli", spec: "@freestyle-sync/auth-github-cli@latest" },
|
|
41
|
+
{ name: "@freestyle-sync/auth-npm", spec: "@freestyle-sync/auth-npm@latest" },
|
|
42
|
+
{ name: "@freestyle-sync/auth-ssh", spec: "@freestyle-sync/auth-ssh@latest" },
|
|
43
|
+
{ name: "@freestyle-sync/auth-yarn", spec: "@freestyle-sync/auth-yarn@latest" },
|
|
44
|
+
{ name: "@freestyle-sync/node-npm", spec: "@freestyle-sync/node-npm@latest" },
|
|
45
|
+
{ name: "@freestyle-sync/shell-history", spec: "@freestyle-sync/shell-history@latest" },
|
|
46
|
+
];
|
|
30
47
|
const pluginUtils = {
|
|
31
48
|
checkedExec,
|
|
32
49
|
createTar,
|
|
@@ -36,6 +53,8 @@ const pluginUtils = {
|
|
|
36
53
|
md5,
|
|
37
54
|
delay,
|
|
38
55
|
};
|
|
56
|
+
let config = { plugins: [] };
|
|
57
|
+
let plugins = config.plugins;
|
|
39
58
|
class Progress {
|
|
40
59
|
current = 0;
|
|
41
60
|
total;
|
|
@@ -68,39 +87,66 @@ class Progress {
|
|
|
68
87
|
}
|
|
69
88
|
if (isDirectCliExecution()) {
|
|
70
89
|
main().catch((error) => {
|
|
71
|
-
console.error(
|
|
90
|
+
console.error(`${CLI_NAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
91
|
process.exitCode = 1;
|
|
73
92
|
});
|
|
74
93
|
}
|
|
75
94
|
async function main() {
|
|
76
95
|
const options = await parseArgs(process.argv.slice(2));
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
const loadedConfig = await loadConfig(options.projectRoot);
|
|
97
|
+
await sync({
|
|
98
|
+
config: loadedConfig,
|
|
99
|
+
options,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export async function sync(sdkOptions) {
|
|
103
|
+
const options = await resolveCliOptions(sdkOptions.options);
|
|
104
|
+
printHeading(CLI_NAME);
|
|
105
|
+
config = sdkOptions.config;
|
|
79
106
|
plugins = config.plugins;
|
|
80
|
-
const pluginPreferences = await updatePluginPreferences(options);
|
|
107
|
+
const pluginPreferences = await updatePluginPreferences(options, sdkOptions.pluginPreferences);
|
|
81
108
|
plugins = activePlugins(pluginPreferences, options);
|
|
109
|
+
let currentCache = sdkOptions.cache ? normalizeCache(sdkOptions.cache, options) : await readCache(options.cachePath, options);
|
|
110
|
+
let resultVmId;
|
|
111
|
+
const saveCache = async (nextCache) => {
|
|
112
|
+
currentCache = nextCache;
|
|
113
|
+
if (sdkOptions.onCacheUpdate) {
|
|
114
|
+
await sdkOptions.onCacheUpdate(nextCache);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!sdkOptions.cache) {
|
|
118
|
+
await writeCache(options.cachePath, nextCache);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
82
121
|
if (options.listPlugins) {
|
|
83
122
|
printPlugins(pluginPreferences, options);
|
|
84
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
cache: currentCache,
|
|
125
|
+
snapshotId: currentCache.snapshotId,
|
|
126
|
+
};
|
|
85
127
|
}
|
|
86
128
|
if (options.dryRun) {
|
|
87
|
-
console.log(
|
|
129
|
+
console.log(`${CLI_NAME} dry run`);
|
|
88
130
|
}
|
|
89
131
|
if (options.skipSync) {
|
|
90
132
|
const progress = new Progress(3);
|
|
91
133
|
progress.step("Reading sync cache");
|
|
92
|
-
const cache =
|
|
134
|
+
const cache = currentCache;
|
|
93
135
|
if (!options.vmId && !cache.snapshotId) {
|
|
94
|
-
console.warn(
|
|
136
|
+
console.warn(`${CLI_NAME} --skip-sync: no cached snapshot found. A new empty VM will be created. Run without --skip-sync first to create a snapshot.`);
|
|
95
137
|
}
|
|
96
138
|
const source = options.vmId ? `existing VM ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : "a new VM";
|
|
97
139
|
console.log(`Skipping sync: creating VM from ${source}`);
|
|
98
140
|
console.log(`Remote project: ${options.remoteProjectDir}`);
|
|
99
141
|
if (options.dryRun) {
|
|
100
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
cache: currentCache,
|
|
144
|
+
snapshotId: currentCache.snapshotId,
|
|
145
|
+
};
|
|
101
146
|
}
|
|
102
147
|
progress.step("Preparing Freestyle VM");
|
|
103
148
|
const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
|
|
149
|
+
resultVmId = vmId;
|
|
104
150
|
console.log(`Using VM: ${vmId}`);
|
|
105
151
|
progress.step(`Running post-sync plugins for ${vmId}`);
|
|
106
152
|
const contextCandidates = await discoverPluginContextCandidates(options);
|
|
@@ -119,11 +165,15 @@ async function main() {
|
|
|
119
165
|
if (!connected)
|
|
120
166
|
await sshIntoVm(vmId);
|
|
121
167
|
}
|
|
122
|
-
return
|
|
168
|
+
return {
|
|
169
|
+
vmId,
|
|
170
|
+
cache: currentCache,
|
|
171
|
+
snapshotId: currentCache.snapshotId,
|
|
172
|
+
};
|
|
123
173
|
}
|
|
124
174
|
const progress = new Progress(options.dryRun ? 2 : options.install ? 12 : 11);
|
|
125
175
|
progress.step("Scanning project files");
|
|
126
|
-
const cache =
|
|
176
|
+
const cache = currentCache;
|
|
127
177
|
const base = cacheBaseForSync(options, cache);
|
|
128
178
|
const projectEntries = await scanProject(options.projectRoot, options.includeGitDir);
|
|
129
179
|
const projectCurrent = digestMap(projectEntries);
|
|
@@ -138,10 +188,14 @@ async function main() {
|
|
|
138
188
|
printPlan(options, projectChanges, contextChanges, envExports, cache);
|
|
139
189
|
if (options.dryRun) {
|
|
140
190
|
progress.finish();
|
|
141
|
-
return
|
|
191
|
+
return {
|
|
192
|
+
cache: currentCache,
|
|
193
|
+
snapshotId: currentCache.snapshotId,
|
|
194
|
+
};
|
|
142
195
|
}
|
|
143
196
|
progress.step("Preparing Freestyle VM");
|
|
144
197
|
const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
|
|
198
|
+
resultVmId = vmId;
|
|
145
199
|
console.log(`Uploading to VM: ${vmId}`);
|
|
146
200
|
progress.step(`Uploading project changes to ${vmId}`);
|
|
147
201
|
await ensureRemoteBase(vm, options.remoteProjectDir);
|
|
@@ -175,14 +229,14 @@ async function main() {
|
|
|
175
229
|
updatedAt: new Date().toISOString(),
|
|
176
230
|
});
|
|
177
231
|
progress.step("Saving local sync cache");
|
|
178
|
-
await
|
|
232
|
+
await saveCache(buildSyncCache());
|
|
179
233
|
let snapshotPromise = null;
|
|
180
234
|
if (options.snapshot) {
|
|
181
235
|
progress.step(`Creating snapshot cache for ${vmId} in background`);
|
|
182
236
|
snapshotPromise = (async () => {
|
|
183
237
|
await runBeforeSnapshotPlugins(vm, vmId, options);
|
|
184
238
|
try {
|
|
185
|
-
const snapshot = await vm.snapshot({ name:
|
|
239
|
+
const snapshot = await vm.snapshot({ name: `${CLI_NAME}-${path.basename(options.projectRoot)}-${Date.now()}` });
|
|
186
240
|
return snapshot.snapshotId;
|
|
187
241
|
}
|
|
188
242
|
catch (error) {
|
|
@@ -219,10 +273,15 @@ async function main() {
|
|
|
219
273
|
if (snapshotPromise) {
|
|
220
274
|
const newSnapshotId = await snapshotPromise;
|
|
221
275
|
if (newSnapshotId) {
|
|
222
|
-
await
|
|
276
|
+
await saveCache(buildSyncCache({ snapshotId: newSnapshotId }));
|
|
223
277
|
console.log(`Snapshot cache saved: ${newSnapshotId}`);
|
|
224
278
|
}
|
|
225
279
|
}
|
|
280
|
+
return {
|
|
281
|
+
vmId: resultVmId,
|
|
282
|
+
cache: currentCache,
|
|
283
|
+
snapshotId: currentCache.snapshotId,
|
|
284
|
+
};
|
|
226
285
|
}
|
|
227
286
|
function isDirectCliExecution() {
|
|
228
287
|
if (!process.argv[1])
|
|
@@ -235,11 +294,7 @@ function isDirectCliExecution() {
|
|
|
235
294
|
}
|
|
236
295
|
}
|
|
237
296
|
async function loadConfig(projectRoot) {
|
|
238
|
-
const configPath =
|
|
239
|
-
if (!(await exists(configPath))) {
|
|
240
|
-
await writeFile(configPath, renderDefaultConfig(), "utf8");
|
|
241
|
-
console.log(`Created ${path.relative(process.cwd(), configPath) || path.basename(configPath)}`);
|
|
242
|
-
}
|
|
297
|
+
const configPath = await resolveConfigPath(projectRoot);
|
|
243
298
|
const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
244
299
|
const loaded = imported.default;
|
|
245
300
|
if (!loaded || !Array.isArray(loaded.plugins)) {
|
|
@@ -247,6 +302,90 @@ async function loadConfig(projectRoot) {
|
|
|
247
302
|
}
|
|
248
303
|
return loaded;
|
|
249
304
|
}
|
|
305
|
+
async function resolveConfigPath(projectRoot) {
|
|
306
|
+
const jsConfigPath = path.join(projectRoot, "freestyle-sync.config.mjs");
|
|
307
|
+
const tsConfigPath = path.join(projectRoot, "freestyle-sync.config.ts");
|
|
308
|
+
if (await exists(jsConfigPath)) {
|
|
309
|
+
if (await migrateBundledDefaultConfig(jsConfigPath)) {
|
|
310
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
311
|
+
}
|
|
312
|
+
else if (await isDefaultGeneratedConfig(jsConfigPath)) {
|
|
313
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
314
|
+
}
|
|
315
|
+
return jsConfigPath;
|
|
316
|
+
}
|
|
317
|
+
if (await exists(tsConfigPath)) {
|
|
318
|
+
if (await migrateLegacyDefaultConfig(tsConfigPath, jsConfigPath)) {
|
|
319
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
320
|
+
return jsConfigPath;
|
|
321
|
+
}
|
|
322
|
+
return tsConfigPath;
|
|
323
|
+
}
|
|
324
|
+
await writeFile(jsConfigPath, renderDefaultConfig(), "utf8");
|
|
325
|
+
console.log(`Created ${path.relative(process.cwd(), jsConfigPath) || path.basename(jsConfigPath)}`);
|
|
326
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
327
|
+
return jsConfigPath;
|
|
328
|
+
}
|
|
329
|
+
async function isDefaultGeneratedConfig(configPath) {
|
|
330
|
+
const contents = await readFile(configPath, "utf8").catch(() => "");
|
|
331
|
+
return contents.trim() === renderDefaultConfig().trim();
|
|
332
|
+
}
|
|
333
|
+
async function migrateBundledDefaultConfig(configPath) {
|
|
334
|
+
const contents = await readFile(configPath, "utf8").catch(() => "");
|
|
335
|
+
if (contents.trim() !== renderBundledDefaultConfig().trim())
|
|
336
|
+
return false;
|
|
337
|
+
await writeFile(configPath, renderDefaultConfig(), "utf8");
|
|
338
|
+
console.log(`Updated ${path.relative(process.cwd(), configPath) || path.basename(configPath)} to install plugin packages`);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
async function migrateLegacyDefaultConfig(tsConfigPath, jsConfigPath) {
|
|
342
|
+
const contents = await readFile(tsConfigPath, "utf8").catch(() => "");
|
|
343
|
+
if (contents.trim() !== renderLegacyDefaultConfig().trim())
|
|
344
|
+
return false;
|
|
345
|
+
await writeFile(jsConfigPath, renderDefaultConfig(), "utf8");
|
|
346
|
+
await rm(tsConfigPath);
|
|
347
|
+
console.log(`Updated ${path.relative(process.cwd(), jsConfigPath) || path.basename(jsConfigPath)} to install plugin packages`);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
function renderLegacyDefaultConfig() {
|
|
351
|
+
return `import { claudeAgentPlugin } from "@freestyle-sync/agent-claude";
|
|
352
|
+
import { codexAgentPlugin } from "@freestyle-sync/agent-codex";
|
|
353
|
+
import { copilotAgentPlugin } from "@freestyle-sync/agent-copilot";
|
|
354
|
+
import { awsAuthPlugin } from "@freestyle-sync/auth-aws";
|
|
355
|
+
import { azureAuthPlugin } from "@freestyle-sync/auth-azure";
|
|
356
|
+
import { dockerAuthPlugin } from "@freestyle-sync/auth-docker";
|
|
357
|
+
import { envAuthPlugin } from "@freestyle-sync/auth-env";
|
|
358
|
+
import { gcloudAuthPlugin } from "@freestyle-sync/auth-gcloud";
|
|
359
|
+
import { gitAuthPlugin } from "@freestyle-sync/auth-git";
|
|
360
|
+
import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
|
|
361
|
+
import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
|
|
362
|
+
import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
|
|
363
|
+
import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
|
|
364
|
+
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
365
|
+
import { shellHistoryPlugin } from "@freestyle-sync/shell-history";
|
|
366
|
+
import { defineConfig } from "freestyle-sync";
|
|
367
|
+
|
|
368
|
+
export default defineConfig({
|
|
369
|
+
plugins: [
|
|
370
|
+
envAuthPlugin(),
|
|
371
|
+
gitAuthPlugin(),
|
|
372
|
+
sshAuthPlugin(),
|
|
373
|
+
githubCliAuthPlugin(),
|
|
374
|
+
npmAuthPlugin(),
|
|
375
|
+
yarnAuthPlugin(),
|
|
376
|
+
dockerAuthPlugin(),
|
|
377
|
+
awsAuthPlugin(),
|
|
378
|
+
azureAuthPlugin(),
|
|
379
|
+
gcloudAuthPlugin(),
|
|
380
|
+
nodeNpmPlugin(),
|
|
381
|
+
claudeAgentPlugin(),
|
|
382
|
+
codexAgentPlugin(),
|
|
383
|
+
copilotAgentPlugin(),
|
|
384
|
+
shellHistoryPlugin(),
|
|
385
|
+
],
|
|
386
|
+
});
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
250
389
|
function renderDefaultConfig() {
|
|
251
390
|
return `import { claudeAgentPlugin } from "@freestyle-sync/agent-claude";
|
|
252
391
|
import { codexAgentPlugin } from "@freestyle-sync/agent-codex";
|
|
@@ -286,28 +425,43 @@ export default defineConfig({
|
|
|
286
425
|
});
|
|
287
426
|
`;
|
|
288
427
|
}
|
|
428
|
+
function renderBundledDefaultConfig() {
|
|
429
|
+
return `export default {
|
|
430
|
+
plugins: "default",
|
|
431
|
+
};
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
async function ensureDefaultConfigDependencies(projectRoot) {
|
|
435
|
+
const missing = [];
|
|
436
|
+
for (const dependency of DEFAULT_CONFIG_DEPENDENCIES) {
|
|
437
|
+
if (!(await isDependencyInstalled(projectRoot, dependency.name))) {
|
|
438
|
+
missing.push(dependency.spec);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (missing.length === 0)
|
|
442
|
+
return;
|
|
443
|
+
const { command, args } = await dependencyInstallCommand(projectRoot, missing);
|
|
444
|
+
console.log(`Installing freestyle-sync config dependencies with ${command}...`);
|
|
445
|
+
try {
|
|
446
|
+
await execFileAsync(command, args, { cwd: projectRoot });
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
const details = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "") : String(error);
|
|
450
|
+
throw new Error(`failed to install freestyle-sync config dependencies: ${details.trim() || String(error)}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function isDependencyInstalled(projectRoot, name) {
|
|
454
|
+
return exists(path.join(projectRoot, "node_modules", ...name.split("/"), "package.json"));
|
|
455
|
+
}
|
|
456
|
+
async function dependencyInstallCommand(projectRoot, specs) {
|
|
457
|
+
if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
|
|
458
|
+
return { command: "pnpm", args: ["add", "-D", ...specs] };
|
|
459
|
+
if (await exists(path.join(projectRoot, "yarn.lock")))
|
|
460
|
+
return { command: "yarn", args: ["add", "-D", ...specs] };
|
|
461
|
+
return { command: "npm", args: ["install", "--save-dev", ...specs] };
|
|
462
|
+
}
|
|
289
463
|
async function parseArgs(args) {
|
|
290
|
-
const options =
|
|
291
|
-
projectRoot: process.cwd(),
|
|
292
|
-
cachePath: "",
|
|
293
|
-
remoteProjectDir: "",
|
|
294
|
-
name: "",
|
|
295
|
-
yes: false,
|
|
296
|
-
dryRun: false,
|
|
297
|
-
disablePlugins: [],
|
|
298
|
-
enablePlugins: [],
|
|
299
|
-
resetPluginPrefs: false,
|
|
300
|
-
listPlugins: false,
|
|
301
|
-
includeAuth: true,
|
|
302
|
-
includeAgentContext: true,
|
|
303
|
-
includeGitDir: true,
|
|
304
|
-
includeAllCopilotWorkspaces: false,
|
|
305
|
-
snapshot: true,
|
|
306
|
-
skipSync: false,
|
|
307
|
-
install: false,
|
|
308
|
-
autoSsh: true,
|
|
309
|
-
envKeys: [],
|
|
310
|
-
};
|
|
464
|
+
const options = defaultCliOptions();
|
|
311
465
|
const positional = [];
|
|
312
466
|
for (let index = 0; index < args.length; index += 1) {
|
|
313
467
|
const arg = args[index];
|
|
@@ -389,25 +543,65 @@ async function parseArgs(args) {
|
|
|
389
543
|
throw new Error("expected at most one project path");
|
|
390
544
|
}
|
|
391
545
|
options.projectRoot = path.resolve(positional[0] ?? options.projectRoot);
|
|
546
|
+
return finalizeCliOptions(options);
|
|
547
|
+
}
|
|
548
|
+
async function resolveCliOptions(overrides) {
|
|
549
|
+
return finalizeCliOptions({
|
|
550
|
+
...defaultCliOptions(),
|
|
551
|
+
...overrides,
|
|
552
|
+
projectRoot: path.resolve(overrides?.projectRoot ?? process.cwd()),
|
|
553
|
+
disablePlugins: overrides?.disablePlugins ? [...overrides.disablePlugins] : [],
|
|
554
|
+
enablePlugins: overrides?.enablePlugins ? [...overrides.enablePlugins] : [],
|
|
555
|
+
envKeys: overrides?.envKeys ? [...overrides.envKeys] : [],
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function defaultCliOptions() {
|
|
559
|
+
return {
|
|
560
|
+
projectRoot: process.cwd(),
|
|
561
|
+
cachePath: "",
|
|
562
|
+
remoteProjectDir: "",
|
|
563
|
+
name: "",
|
|
564
|
+
yes: false,
|
|
565
|
+
dryRun: false,
|
|
566
|
+
disablePlugins: [],
|
|
567
|
+
enablePlugins: [],
|
|
568
|
+
resetPluginPrefs: false,
|
|
569
|
+
listPlugins: false,
|
|
570
|
+
includeAuth: true,
|
|
571
|
+
includeAgentContext: true,
|
|
572
|
+
includeGitDir: true,
|
|
573
|
+
includeAllCopilotWorkspaces: false,
|
|
574
|
+
snapshot: true,
|
|
575
|
+
skipSync: false,
|
|
576
|
+
install: false,
|
|
577
|
+
autoSsh: true,
|
|
578
|
+
envKeys: [],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function finalizeCliOptions(options) {
|
|
392
582
|
const projectStats = await stat(options.projectRoot).catch(() => null);
|
|
393
583
|
if (!projectStats?.isDirectory()) {
|
|
394
584
|
throw new Error(`project path is not a directory: ${options.projectRoot}`);
|
|
395
585
|
}
|
|
396
586
|
const projectName = sanitizeName(path.basename(options.projectRoot));
|
|
397
|
-
options.
|
|
398
|
-
options.
|
|
399
|
-
|
|
400
|
-
|
|
587
|
+
const remoteProjectDir = options.remoteProjectDir ? normalizeRemotePath(options.remoteProjectDir) : defaultRemoteProjectDir(options.projectRoot);
|
|
588
|
+
const cachePath = options.cachePath ? path.resolve(options.cachePath) : path.join(options.projectRoot, ".freestyle-sync", "cache.json");
|
|
589
|
+
return {
|
|
590
|
+
...options,
|
|
591
|
+
name: options.name || `${CLI_NAME}-${projectName}`,
|
|
592
|
+
remoteProjectDir,
|
|
593
|
+
cachePath,
|
|
594
|
+
};
|
|
401
595
|
}
|
|
402
596
|
function printHelp() {
|
|
403
|
-
console.log(
|
|
597
|
+
console.log(`${CLI_NAME} uploads the current project into a Freestyle VM.
|
|
404
598
|
|
|
405
599
|
Usage:
|
|
406
|
-
|
|
600
|
+
${CLI_NAME} [project-dir] [options]
|
|
407
601
|
|
|
408
602
|
Options:
|
|
409
603
|
--vm-id <id> Sync into an existing Freestyle VM.
|
|
410
|
-
|
|
604
|
+
--name <name> Name for a newly created VM. Defaults to ${CLI_NAME}-<project>.
|
|
411
605
|
--remote-dir <path> Remote project directory. Defaults to the local absolute path.
|
|
412
606
|
--cache <path> Snapshot/hash cache path. Defaults to .freestyle-sync/cache.json.
|
|
413
607
|
--include-env <name> Always copy an environment variable. Repeatable.
|
|
@@ -464,6 +658,18 @@ async function readCache(cachePath, options) {
|
|
|
464
658
|
contextFiles: {},
|
|
465
659
|
};
|
|
466
660
|
}
|
|
661
|
+
function normalizeCache(cache, options) {
|
|
662
|
+
return {
|
|
663
|
+
...cache,
|
|
664
|
+
version: CACHE_VERSION,
|
|
665
|
+
projectRoot: options.projectRoot,
|
|
666
|
+
remoteProjectDir: options.remoteProjectDir,
|
|
667
|
+
projectFiles: cache.projectFiles ?? {},
|
|
668
|
+
contextFiles: cache.contextFiles ?? {},
|
|
669
|
+
snapshotProjectFiles: cache.snapshotProjectFiles ?? {},
|
|
670
|
+
snapshotContextFiles: cache.snapshotContextFiles ?? {},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
467
673
|
function cacheBaseForSync(options, cache) {
|
|
468
674
|
if (options.vmId && options.vmId === cache.vmId) {
|
|
469
675
|
return {
|
|
@@ -632,10 +838,13 @@ async function scanContextCandidates(candidates) {
|
|
|
632
838
|
}
|
|
633
839
|
return entries.sort((left, right) => left.remotePath.localeCompare(right.remotePath));
|
|
634
840
|
}
|
|
635
|
-
async function updatePluginPreferences(options) {
|
|
841
|
+
async function updatePluginPreferences(options, providedPreferences) {
|
|
636
842
|
const preferencesPath = getPluginPreferencesPath(options);
|
|
637
|
-
const
|
|
638
|
-
|
|
843
|
+
const usingProvidedPreferences = Boolean(providedPreferences);
|
|
844
|
+
const preferences = options.resetPluginPrefs
|
|
845
|
+
? emptyPluginPreferences()
|
|
846
|
+
: normalizePluginPreferences(providedPreferences ?? await readPluginPreferences(preferencesPath));
|
|
847
|
+
let changed = options.resetPluginPrefs && !usingProvidedPreferences;
|
|
639
848
|
const disabledPlugins = new Set();
|
|
640
849
|
for (const savedName of preferences.disabledPlugins) {
|
|
641
850
|
const canonicalName = maybeResolvePluginSelector(savedName);
|
|
@@ -656,7 +865,7 @@ async function updatePluginPreferences(options) {
|
|
|
656
865
|
}
|
|
657
866
|
}
|
|
658
867
|
const next = { version: PLUGIN_PREFERENCES_VERSION, disabledPlugins: [...disabledPlugins].sort(), updatedAt: preferences.updatedAt };
|
|
659
|
-
if (changed)
|
|
868
|
+
if (changed && !usingProvidedPreferences)
|
|
660
869
|
await writePluginPreferences(preferencesPath, next);
|
|
661
870
|
return next;
|
|
662
871
|
}
|
|
@@ -719,6 +928,13 @@ function pluginSelectorAliases(name) {
|
|
|
719
928
|
function emptyPluginPreferences() {
|
|
720
929
|
return { version: PLUGIN_PREFERENCES_VERSION, disabledPlugins: [] };
|
|
721
930
|
}
|
|
931
|
+
function normalizePluginPreferences(preferences) {
|
|
932
|
+
return {
|
|
933
|
+
version: PLUGIN_PREFERENCES_VERSION,
|
|
934
|
+
disabledPlugins: Array.isArray(preferences.disabledPlugins) ? preferences.disabledPlugins.filter((name) => typeof name === "string") : [],
|
|
935
|
+
updatedAt: preferences.updatedAt,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
722
938
|
function getPluginPreferencesPath(options) {
|
|
723
939
|
return path.join(path.dirname(options.cachePath), "plugin-preferences.json");
|
|
724
940
|
}
|
|
@@ -726,11 +942,7 @@ async function readPluginPreferences(preferencesPath) {
|
|
|
726
942
|
try {
|
|
727
943
|
const parsed = JSON.parse(await readFile(preferencesPath, "utf8"));
|
|
728
944
|
if (parsed.version === PLUGIN_PREFERENCES_VERSION) {
|
|
729
|
-
return
|
|
730
|
-
version: PLUGIN_PREFERENCES_VERSION,
|
|
731
|
-
disabledPlugins: Array.isArray(parsed.disabledPlugins) ? parsed.disabledPlugins.filter((name) => typeof name === "string") : [],
|
|
732
|
-
updatedAt: parsed.updatedAt,
|
|
733
|
-
};
|
|
945
|
+
return normalizePluginPreferences(parsed);
|
|
734
946
|
}
|
|
735
947
|
}
|
|
736
948
|
catch (error) {
|
|
@@ -815,8 +1027,8 @@ async function syncProject(vm, vmId, options, changes) {
|
|
|
815
1027
|
console.log(`VM ${vmId}: uploading ${changes.changed.length} changed project files...`);
|
|
816
1028
|
const archive = await createProjectArchive(options.projectRoot, changes.changed);
|
|
817
1029
|
try {
|
|
818
|
-
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/
|
|
819
|
-
await checkedExec(vm, `mkdir -p ${shellQuote(options.remoteProjectDir)} && tar --no-same-owner --no-same-permissions -xzf /tmp/
|
|
1030
|
+
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-project.tgz", "project");
|
|
1031
|
+
await checkedExec(vm, `mkdir -p ${shellQuote(options.remoteProjectDir)} && tar --no-same-owner --no-same-permissions -xzf /tmp/freestyle-sync-project.tgz -C ${shellQuote(options.remoteProjectDir)} && rm -f /tmp/freestyle-sync-project.tgz`);
|
|
820
1032
|
}
|
|
821
1033
|
finally {
|
|
822
1034
|
await rm(path.dirname(archive), { recursive: true, force: true });
|
|
@@ -825,8 +1037,8 @@ async function syncProject(vm, vmId, options, changes) {
|
|
|
825
1037
|
if (changes.removed.length > 0) {
|
|
826
1038
|
console.log(`VM ${vmId}: removing ${changes.removed.length} deleted project files...`);
|
|
827
1039
|
const removeList = Buffer.from(changes.removed.join("\0") + "\0");
|
|
828
|
-
await vm.fs.writeFile("/tmp/
|
|
829
|
-
await checkedExec(vm, `cd ${shellQuote(options.remoteProjectDir)} && xargs -0 rm -f -- < /tmp/
|
|
1040
|
+
await vm.fs.writeFile("/tmp/freestyle-sync-remove-list", removeList);
|
|
1041
|
+
await checkedExec(vm, `cd ${shellQuote(options.remoteProjectDir)} && xargs -0 rm -f -- < /tmp/freestyle-sync-remove-list && rm -f /tmp/freestyle-sync-remove-list`);
|
|
830
1042
|
}
|
|
831
1043
|
return {
|
|
832
1044
|
uploaded: changes.changed.length,
|
|
@@ -885,8 +1097,8 @@ async function syncContext(vm, vmId, changes) {
|
|
|
885
1097
|
console.log(`VM ${vmId}: uploading ${changes.changed.length} changed auth/context files...`);
|
|
886
1098
|
const archive = await createContextArchive(changes.changed);
|
|
887
1099
|
try {
|
|
888
|
-
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/
|
|
889
|
-
await checkedExec(vm, "tar --no-same-owner --no-same-permissions -xzf /tmp/
|
|
1100
|
+
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-context.tgz", "context");
|
|
1101
|
+
await checkedExec(vm, "tar --no-same-owner --no-same-permissions -xzf /tmp/freestyle-sync-context.tgz -C / && rm -f /tmp/freestyle-sync-context.tgz");
|
|
890
1102
|
}
|
|
891
1103
|
finally {
|
|
892
1104
|
await rm(path.dirname(archive), { recursive: true, force: true });
|
|
@@ -954,7 +1166,7 @@ async function detectInstallCommand(projectRoot) {
|
|
|
954
1166
|
return undefined;
|
|
955
1167
|
}
|
|
956
1168
|
async function createProjectArchive(projectRoot, entries) {
|
|
957
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "
|
|
1169
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-"));
|
|
958
1170
|
const listPath = path.join(tempDir, "files.list");
|
|
959
1171
|
const archivePath = path.join(tempDir, "project.tgz");
|
|
960
1172
|
await writeFile(listPath, Buffer.from(entries.map((entry) => entry.relativePath).join("\0") + "\0"));
|
|
@@ -962,7 +1174,7 @@ async function createProjectArchive(projectRoot, entries) {
|
|
|
962
1174
|
return archivePath;
|
|
963
1175
|
}
|
|
964
1176
|
async function createContextArchive(entries) {
|
|
965
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "
|
|
1177
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-context-"));
|
|
966
1178
|
const stagingDir = path.join(tempDir, "staging");
|
|
967
1179
|
const archivePath = path.join(tempDir, "context.tgz");
|
|
968
1180
|
await mkdir(stagingDir, { recursive: true });
|
|
@@ -987,17 +1199,29 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
987
1199
|
const archive = await readFile(archivePath);
|
|
988
1200
|
const encoded = archive.toString("base64");
|
|
989
1201
|
const chunkCount = Math.max(1, Math.ceil(encoded.length / ARCHIVE_CHUNK_CHARS));
|
|
990
|
-
const chunkDir = `/tmp/
|
|
1202
|
+
const chunkDir = `/tmp/freestyle-sync-${label}-${Date.now()}.chunks`;
|
|
991
1203
|
console.log(`VM ${vmId}: streaming ${formatBytes(archive.length)} ${label} archive in ${chunkCount} chunk${chunkCount === 1 ? "" : "s"}...`);
|
|
992
1204
|
await checkedExec(vm, `rm -rf ${shellQuote(chunkDir)} && mkdir -p ${shellQuote(chunkDir)}`);
|
|
993
1205
|
const width = String(chunkCount - 1).length;
|
|
1206
|
+
const canRenderInlineProgress = process.stdout.isTTY;
|
|
1207
|
+
const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
|
|
994
1208
|
for (let index = 0; index < chunkCount; index += 1) {
|
|
995
1209
|
const start = index * ARCHIVE_CHUNK_CHARS;
|
|
996
1210
|
const chunk = encoded.slice(start, start + ARCHIVE_CHUNK_CHARS);
|
|
997
1211
|
const chunkName = `${String(index).padStart(width, "0")}.b64`;
|
|
998
1212
|
await vm.fs.writeTextFile(`${chunkDir}/${chunkName}`, chunk);
|
|
999
|
-
|
|
1000
|
-
|
|
1213
|
+
const uploadedChunks = index + 1;
|
|
1214
|
+
if (chunkCount > 1) {
|
|
1215
|
+
const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
|
|
1216
|
+
if (canRenderInlineProgress) {
|
|
1217
|
+
process.stdout.write(`\r${progressMessage}`);
|
|
1218
|
+
if (uploadedChunks === chunkCount) {
|
|
1219
|
+
process.stdout.write("\n");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
else if (uploadedChunks === 1 || uploadedChunks % logEvery === 0 || uploadedChunks === chunkCount) {
|
|
1223
|
+
console.log(progressMessage);
|
|
1224
|
+
}
|
|
1001
1225
|
}
|
|
1002
1226
|
}
|
|
1003
1227
|
await checkedExec(vm, `cat ${shellQuote(chunkDir)}/*.b64 | base64 -d > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`);
|
|
@@ -1095,7 +1319,7 @@ function md5(value) {
|
|
|
1095
1319
|
return createHash("md5").update(value).digest("hex");
|
|
1096
1320
|
}
|
|
1097
1321
|
function renderEnvFile(envExports) {
|
|
1098
|
-
const lines = [
|
|
1322
|
+
const lines = [`# Generated by ${CLI_NAME}. Contains local auth tokens.`];
|
|
1099
1323
|
for (const key of Object.keys(envExports).sort()) {
|
|
1100
1324
|
lines.push(`export ${key}=${shellQuote(envExports[key])}`);
|
|
1101
1325
|
}
|
|
@@ -1115,7 +1339,28 @@ function defaultRemoteProjectDir(projectRoot) {
|
|
|
1115
1339
|
return normalizeRemotePath(toPosix(projectRoot));
|
|
1116
1340
|
}
|
|
1117
1341
|
function sanitizeName(value) {
|
|
1118
|
-
|
|
1342
|
+
const lower = value.toLowerCase();
|
|
1343
|
+
let normalized = "";
|
|
1344
|
+
let lastWasDash = false;
|
|
1345
|
+
for (const character of lower) {
|
|
1346
|
+
const isAlpha = character >= "a" && character <= "z";
|
|
1347
|
+
const isDigit = character >= "0" && character <= "9";
|
|
1348
|
+
const isAllowedPunctuation = character === "." || character === "_" || character === "-";
|
|
1349
|
+
if (isAlpha || isDigit || isAllowedPunctuation) {
|
|
1350
|
+
normalized += character;
|
|
1351
|
+
lastWasDash = false;
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (!lastWasDash) {
|
|
1355
|
+
normalized += "-";
|
|
1356
|
+
lastWasDash = true;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
while (normalized.startsWith("-"))
|
|
1360
|
+
normalized = normalized.slice(1);
|
|
1361
|
+
while (normalized.endsWith("-"))
|
|
1362
|
+
normalized = normalized.slice(0, -1);
|
|
1363
|
+
return normalized || "project";
|
|
1119
1364
|
}
|
|
1120
1365
|
function toPosix(value) {
|
|
1121
1366
|
return value.split(path.sep).join(path.posix.sep);
|