freestyle-sync 0.1.4 → 0.1.7
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 +515 -74
- package/package.json +18 -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/dist/src/main.js
CHANGED
|
@@ -11,22 +11,44 @@ import "dotenv/config";
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { createReadStream, realpathSync } from "node:fs";
|
|
13
13
|
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
14
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
17
|
import { execFile, spawn } from "node:child_process";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
|
-
import {
|
|
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;
|
|
27
|
+
const DEFAULT_FREESTYLE_API_URL = "https://api.freestyle.sh";
|
|
28
|
+
const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
|
|
29
|
+
const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
|
|
30
|
+
const DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY = "pck_h2aft7g9pqjzrkdnzs199h1may5wjtdtdxeex7m2wzp1r";
|
|
31
|
+
const STACK_REFRESH_TOKEN_ENV_KEY = "FREESTYLE_STACK_REFRESH_TOKEN";
|
|
26
32
|
const USE_UNICODE_OUTPUT = process.stdout.isTTY && (process.env.TERM !== "dumb" || Boolean(process.env.TERM_PROGRAM));
|
|
27
33
|
const USE_STYLED_OUTPUT = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const DEFAULT_CONFIG_DEPENDENCIES = [
|
|
35
|
+
{ name: "freestyle-sync", spec: "freestyle-sync@latest" },
|
|
36
|
+
{ name: "@freestyle-sync/agent-claude", spec: "@freestyle-sync/agent-claude@latest" },
|
|
37
|
+
{ name: "@freestyle-sync/agent-codex", spec: "@freestyle-sync/agent-codex@latest" },
|
|
38
|
+
{ name: "@freestyle-sync/agent-copilot", spec: "@freestyle-sync/agent-copilot@latest" },
|
|
39
|
+
{ name: "@freestyle-sync/auth-aws", spec: "@freestyle-sync/auth-aws@latest" },
|
|
40
|
+
{ name: "@freestyle-sync/auth-azure", spec: "@freestyle-sync/auth-azure@latest" },
|
|
41
|
+
{ name: "@freestyle-sync/auth-docker", spec: "@freestyle-sync/auth-docker@latest" },
|
|
42
|
+
{ name: "@freestyle-sync/auth-env", spec: "@freestyle-sync/auth-env@latest" },
|
|
43
|
+
{ name: "@freestyle-sync/auth-gcloud", spec: "@freestyle-sync/auth-gcloud@latest" },
|
|
44
|
+
{ name: "@freestyle-sync/auth-git", spec: "@freestyle-sync/auth-git@latest" },
|
|
45
|
+
{ name: "@freestyle-sync/auth-github-cli", spec: "@freestyle-sync/auth-github-cli@latest" },
|
|
46
|
+
{ name: "@freestyle-sync/auth-npm", spec: "@freestyle-sync/auth-npm@latest" },
|
|
47
|
+
{ name: "@freestyle-sync/auth-ssh", spec: "@freestyle-sync/auth-ssh@latest" },
|
|
48
|
+
{ name: "@freestyle-sync/auth-yarn", spec: "@freestyle-sync/auth-yarn@latest" },
|
|
49
|
+
{ name: "@freestyle-sync/node-npm", spec: "@freestyle-sync/node-npm@latest" },
|
|
50
|
+
{ name: "@freestyle-sync/shell-history", spec: "@freestyle-sync/shell-history@latest" },
|
|
51
|
+
];
|
|
30
52
|
const pluginUtils = {
|
|
31
53
|
checkedExec,
|
|
32
54
|
createTar,
|
|
@@ -36,6 +58,8 @@ const pluginUtils = {
|
|
|
36
58
|
md5,
|
|
37
59
|
delay,
|
|
38
60
|
};
|
|
61
|
+
let config = { plugins: [] };
|
|
62
|
+
let plugins = config.plugins;
|
|
39
63
|
class Progress {
|
|
40
64
|
current = 0;
|
|
41
65
|
total;
|
|
@@ -68,39 +92,66 @@ class Progress {
|
|
|
68
92
|
}
|
|
69
93
|
if (isDirectCliExecution()) {
|
|
70
94
|
main().catch((error) => {
|
|
71
|
-
console.error(
|
|
95
|
+
console.error(`${CLI_NAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
96
|
process.exitCode = 1;
|
|
73
97
|
});
|
|
74
98
|
}
|
|
75
99
|
async function main() {
|
|
76
100
|
const options = await parseArgs(process.argv.slice(2));
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
const loadedConfig = await loadConfig(options.projectRoot);
|
|
102
|
+
await sync({
|
|
103
|
+
config: loadedConfig,
|
|
104
|
+
options,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export async function sync(sdkOptions) {
|
|
108
|
+
const options = await resolveCliOptions(sdkOptions.options);
|
|
109
|
+
printHeading(CLI_NAME);
|
|
110
|
+
config = sdkOptions.config;
|
|
79
111
|
plugins = config.plugins;
|
|
80
|
-
const pluginPreferences = await updatePluginPreferences(options);
|
|
112
|
+
const pluginPreferences = await updatePluginPreferences(options, sdkOptions.pluginPreferences);
|
|
81
113
|
plugins = activePlugins(pluginPreferences, options);
|
|
114
|
+
let currentCache = sdkOptions.cache ? normalizeCache(sdkOptions.cache, options) : await readCache(options.cachePath, options);
|
|
115
|
+
let resultVmId;
|
|
116
|
+
const saveCache = async (nextCache) => {
|
|
117
|
+
currentCache = nextCache;
|
|
118
|
+
if (sdkOptions.onCacheUpdate) {
|
|
119
|
+
await sdkOptions.onCacheUpdate(nextCache);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!sdkOptions.cache) {
|
|
123
|
+
await writeCache(options.cachePath, nextCache);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
82
126
|
if (options.listPlugins) {
|
|
83
127
|
printPlugins(pluginPreferences, options);
|
|
84
|
-
return
|
|
128
|
+
return {
|
|
129
|
+
cache: currentCache,
|
|
130
|
+
snapshotId: currentCache.snapshotId,
|
|
131
|
+
};
|
|
85
132
|
}
|
|
86
133
|
if (options.dryRun) {
|
|
87
|
-
console.log(
|
|
134
|
+
console.log(`${CLI_NAME} dry run`);
|
|
88
135
|
}
|
|
89
136
|
if (options.skipSync) {
|
|
90
137
|
const progress = new Progress(3);
|
|
91
138
|
progress.step("Reading sync cache");
|
|
92
|
-
const cache =
|
|
139
|
+
const cache = currentCache;
|
|
93
140
|
if (!options.vmId && !cache.snapshotId) {
|
|
94
|
-
console.warn(
|
|
141
|
+
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
142
|
}
|
|
96
143
|
const source = options.vmId ? `existing VM ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : "a new VM";
|
|
97
144
|
console.log(`Skipping sync: creating VM from ${source}`);
|
|
98
145
|
console.log(`Remote project: ${options.remoteProjectDir}`);
|
|
99
146
|
if (options.dryRun) {
|
|
100
|
-
return
|
|
147
|
+
return {
|
|
148
|
+
cache: currentCache,
|
|
149
|
+
snapshotId: currentCache.snapshotId,
|
|
150
|
+
};
|
|
101
151
|
}
|
|
102
152
|
progress.step("Preparing Freestyle VM");
|
|
103
153
|
const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
|
|
154
|
+
resultVmId = vmId;
|
|
104
155
|
console.log(`Using VM: ${vmId}`);
|
|
105
156
|
progress.step(`Running post-sync plugins for ${vmId}`);
|
|
106
157
|
const contextCandidates = await discoverPluginContextCandidates(options);
|
|
@@ -119,11 +170,15 @@ async function main() {
|
|
|
119
170
|
if (!connected)
|
|
120
171
|
await sshIntoVm(vmId);
|
|
121
172
|
}
|
|
122
|
-
return
|
|
173
|
+
return {
|
|
174
|
+
vmId,
|
|
175
|
+
cache: currentCache,
|
|
176
|
+
snapshotId: currentCache.snapshotId,
|
|
177
|
+
};
|
|
123
178
|
}
|
|
124
179
|
const progress = new Progress(options.dryRun ? 2 : options.install ? 12 : 11);
|
|
125
180
|
progress.step("Scanning project files");
|
|
126
|
-
const cache =
|
|
181
|
+
const cache = currentCache;
|
|
127
182
|
const base = cacheBaseForSync(options, cache);
|
|
128
183
|
const projectEntries = await scanProject(options.projectRoot, options.includeGitDir);
|
|
129
184
|
const projectCurrent = digestMap(projectEntries);
|
|
@@ -138,10 +193,14 @@ async function main() {
|
|
|
138
193
|
printPlan(options, projectChanges, contextChanges, envExports, cache);
|
|
139
194
|
if (options.dryRun) {
|
|
140
195
|
progress.finish();
|
|
141
|
-
return
|
|
196
|
+
return {
|
|
197
|
+
cache: currentCache,
|
|
198
|
+
snapshotId: currentCache.snapshotId,
|
|
199
|
+
};
|
|
142
200
|
}
|
|
143
201
|
progress.step("Preparing Freestyle VM");
|
|
144
202
|
const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
|
|
203
|
+
resultVmId = vmId;
|
|
145
204
|
console.log(`Uploading to VM: ${vmId}`);
|
|
146
205
|
progress.step(`Uploading project changes to ${vmId}`);
|
|
147
206
|
await ensureRemoteBase(vm, options.remoteProjectDir);
|
|
@@ -175,14 +234,14 @@ async function main() {
|
|
|
175
234
|
updatedAt: new Date().toISOString(),
|
|
176
235
|
});
|
|
177
236
|
progress.step("Saving local sync cache");
|
|
178
|
-
await
|
|
237
|
+
await saveCache(buildSyncCache());
|
|
179
238
|
let snapshotPromise = null;
|
|
180
239
|
if (options.snapshot) {
|
|
181
240
|
progress.step(`Creating snapshot cache for ${vmId} in background`);
|
|
182
241
|
snapshotPromise = (async () => {
|
|
183
242
|
await runBeforeSnapshotPlugins(vm, vmId, options);
|
|
184
243
|
try {
|
|
185
|
-
const snapshot = await vm.snapshot({ name:
|
|
244
|
+
const snapshot = await vm.snapshot({ name: `${CLI_NAME}-${path.basename(options.projectRoot)}-${Date.now()}` });
|
|
186
245
|
return snapshot.snapshotId;
|
|
187
246
|
}
|
|
188
247
|
catch (error) {
|
|
@@ -219,10 +278,15 @@ async function main() {
|
|
|
219
278
|
if (snapshotPromise) {
|
|
220
279
|
const newSnapshotId = await snapshotPromise;
|
|
221
280
|
if (newSnapshotId) {
|
|
222
|
-
await
|
|
281
|
+
await saveCache(buildSyncCache({ snapshotId: newSnapshotId }));
|
|
223
282
|
console.log(`Snapshot cache saved: ${newSnapshotId}`);
|
|
224
283
|
}
|
|
225
284
|
}
|
|
285
|
+
return {
|
|
286
|
+
vmId: resultVmId,
|
|
287
|
+
cache: currentCache,
|
|
288
|
+
snapshotId: currentCache.snapshotId,
|
|
289
|
+
};
|
|
226
290
|
}
|
|
227
291
|
function isDirectCliExecution() {
|
|
228
292
|
if (!process.argv[1])
|
|
@@ -235,11 +299,7 @@ function isDirectCliExecution() {
|
|
|
235
299
|
}
|
|
236
300
|
}
|
|
237
301
|
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
|
-
}
|
|
302
|
+
const configPath = await resolveConfigPath(projectRoot);
|
|
243
303
|
const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
244
304
|
const loaded = imported.default;
|
|
245
305
|
if (!loaded || !Array.isArray(loaded.plugins)) {
|
|
@@ -247,6 +307,90 @@ async function loadConfig(projectRoot) {
|
|
|
247
307
|
}
|
|
248
308
|
return loaded;
|
|
249
309
|
}
|
|
310
|
+
async function resolveConfigPath(projectRoot) {
|
|
311
|
+
const jsConfigPath = path.join(projectRoot, "freestyle-sync.config.mjs");
|
|
312
|
+
const tsConfigPath = path.join(projectRoot, "freestyle-sync.config.ts");
|
|
313
|
+
if (await exists(jsConfigPath)) {
|
|
314
|
+
if (await migrateBundledDefaultConfig(jsConfigPath)) {
|
|
315
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
316
|
+
}
|
|
317
|
+
else if (await isDefaultGeneratedConfig(jsConfigPath)) {
|
|
318
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
319
|
+
}
|
|
320
|
+
return jsConfigPath;
|
|
321
|
+
}
|
|
322
|
+
if (await exists(tsConfigPath)) {
|
|
323
|
+
if (await migrateLegacyDefaultConfig(tsConfigPath, jsConfigPath)) {
|
|
324
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
325
|
+
return jsConfigPath;
|
|
326
|
+
}
|
|
327
|
+
return tsConfigPath;
|
|
328
|
+
}
|
|
329
|
+
await writeFile(jsConfigPath, renderDefaultConfig(), "utf8");
|
|
330
|
+
console.log(`Created ${path.relative(process.cwd(), jsConfigPath) || path.basename(jsConfigPath)}`);
|
|
331
|
+
await ensureDefaultConfigDependencies(projectRoot);
|
|
332
|
+
return jsConfigPath;
|
|
333
|
+
}
|
|
334
|
+
async function isDefaultGeneratedConfig(configPath) {
|
|
335
|
+
const contents = await readFile(configPath, "utf8").catch(() => "");
|
|
336
|
+
return contents.trim() === renderDefaultConfig().trim();
|
|
337
|
+
}
|
|
338
|
+
async function migrateBundledDefaultConfig(configPath) {
|
|
339
|
+
const contents = await readFile(configPath, "utf8").catch(() => "");
|
|
340
|
+
if (contents.trim() !== renderBundledDefaultConfig().trim())
|
|
341
|
+
return false;
|
|
342
|
+
await writeFile(configPath, renderDefaultConfig(), "utf8");
|
|
343
|
+
console.log(`Updated ${path.relative(process.cwd(), configPath) || path.basename(configPath)} to install plugin packages`);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
async function migrateLegacyDefaultConfig(tsConfigPath, jsConfigPath) {
|
|
347
|
+
const contents = await readFile(tsConfigPath, "utf8").catch(() => "");
|
|
348
|
+
if (contents.trim() !== renderLegacyDefaultConfig().trim())
|
|
349
|
+
return false;
|
|
350
|
+
await writeFile(jsConfigPath, renderDefaultConfig(), "utf8");
|
|
351
|
+
await rm(tsConfigPath);
|
|
352
|
+
console.log(`Updated ${path.relative(process.cwd(), jsConfigPath) || path.basename(jsConfigPath)} to install plugin packages`);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
function renderLegacyDefaultConfig() {
|
|
356
|
+
return `import { claudeAgentPlugin } from "@freestyle-sync/agent-claude";
|
|
357
|
+
import { codexAgentPlugin } from "@freestyle-sync/agent-codex";
|
|
358
|
+
import { copilotAgentPlugin } from "@freestyle-sync/agent-copilot";
|
|
359
|
+
import { awsAuthPlugin } from "@freestyle-sync/auth-aws";
|
|
360
|
+
import { azureAuthPlugin } from "@freestyle-sync/auth-azure";
|
|
361
|
+
import { dockerAuthPlugin } from "@freestyle-sync/auth-docker";
|
|
362
|
+
import { envAuthPlugin } from "@freestyle-sync/auth-env";
|
|
363
|
+
import { gcloudAuthPlugin } from "@freestyle-sync/auth-gcloud";
|
|
364
|
+
import { gitAuthPlugin } from "@freestyle-sync/auth-git";
|
|
365
|
+
import { githubCliAuthPlugin } from "@freestyle-sync/auth-github-cli";
|
|
366
|
+
import { npmAuthPlugin } from "@freestyle-sync/auth-npm";
|
|
367
|
+
import { sshAuthPlugin } from "@freestyle-sync/auth-ssh";
|
|
368
|
+
import { yarnAuthPlugin } from "@freestyle-sync/auth-yarn";
|
|
369
|
+
import { nodeNpmPlugin } from "@freestyle-sync/node-npm";
|
|
370
|
+
import { shellHistoryPlugin } from "@freestyle-sync/shell-history";
|
|
371
|
+
import { defineConfig } from "freestyle-sync";
|
|
372
|
+
|
|
373
|
+
export default defineConfig({
|
|
374
|
+
plugins: [
|
|
375
|
+
envAuthPlugin(),
|
|
376
|
+
gitAuthPlugin(),
|
|
377
|
+
sshAuthPlugin(),
|
|
378
|
+
githubCliAuthPlugin(),
|
|
379
|
+
npmAuthPlugin(),
|
|
380
|
+
yarnAuthPlugin(),
|
|
381
|
+
dockerAuthPlugin(),
|
|
382
|
+
awsAuthPlugin(),
|
|
383
|
+
azureAuthPlugin(),
|
|
384
|
+
gcloudAuthPlugin(),
|
|
385
|
+
nodeNpmPlugin(),
|
|
386
|
+
claudeAgentPlugin(),
|
|
387
|
+
codexAgentPlugin(),
|
|
388
|
+
copilotAgentPlugin(),
|
|
389
|
+
shellHistoryPlugin(),
|
|
390
|
+
],
|
|
391
|
+
});
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
250
394
|
function renderDefaultConfig() {
|
|
251
395
|
return `import { claudeAgentPlugin } from "@freestyle-sync/agent-claude";
|
|
252
396
|
import { codexAgentPlugin } from "@freestyle-sync/agent-codex";
|
|
@@ -286,28 +430,43 @@ export default defineConfig({
|
|
|
286
430
|
});
|
|
287
431
|
`;
|
|
288
432
|
}
|
|
433
|
+
function renderBundledDefaultConfig() {
|
|
434
|
+
return `export default {
|
|
435
|
+
plugins: "default",
|
|
436
|
+
};
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
async function ensureDefaultConfigDependencies(projectRoot) {
|
|
440
|
+
const missing = [];
|
|
441
|
+
for (const dependency of DEFAULT_CONFIG_DEPENDENCIES) {
|
|
442
|
+
if (!(await isDependencyInstalled(projectRoot, dependency.name))) {
|
|
443
|
+
missing.push(dependency.spec);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (missing.length === 0)
|
|
447
|
+
return;
|
|
448
|
+
const { command, args } = await dependencyInstallCommand(projectRoot, missing);
|
|
449
|
+
console.log(`Installing freestyle-sync config dependencies with ${command}...`);
|
|
450
|
+
try {
|
|
451
|
+
await execFileAsync(command, args, { cwd: projectRoot });
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
const details = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "") : String(error);
|
|
455
|
+
throw new Error(`failed to install freestyle-sync config dependencies: ${details.trim() || String(error)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function isDependencyInstalled(projectRoot, name) {
|
|
459
|
+
return exists(path.join(projectRoot, "node_modules", ...name.split("/"), "package.json"));
|
|
460
|
+
}
|
|
461
|
+
async function dependencyInstallCommand(projectRoot, specs) {
|
|
462
|
+
if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
|
|
463
|
+
return { command: "pnpm", args: ["add", "-D", ...specs] };
|
|
464
|
+
if (await exists(path.join(projectRoot, "yarn.lock")))
|
|
465
|
+
return { command: "yarn", args: ["add", "-D", ...specs] };
|
|
466
|
+
return { command: "npm", args: ["install", "--save-dev", ...specs] };
|
|
467
|
+
}
|
|
289
468
|
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
|
-
};
|
|
469
|
+
const options = defaultCliOptions();
|
|
311
470
|
const positional = [];
|
|
312
471
|
for (let index = 0; index < args.length; index += 1) {
|
|
313
472
|
const arg = args[index];
|
|
@@ -389,25 +548,65 @@ async function parseArgs(args) {
|
|
|
389
548
|
throw new Error("expected at most one project path");
|
|
390
549
|
}
|
|
391
550
|
options.projectRoot = path.resolve(positional[0] ?? options.projectRoot);
|
|
551
|
+
return finalizeCliOptions(options);
|
|
552
|
+
}
|
|
553
|
+
async function resolveCliOptions(overrides) {
|
|
554
|
+
return finalizeCliOptions({
|
|
555
|
+
...defaultCliOptions(),
|
|
556
|
+
...overrides,
|
|
557
|
+
projectRoot: path.resolve(overrides?.projectRoot ?? process.cwd()),
|
|
558
|
+
disablePlugins: overrides?.disablePlugins ? [...overrides.disablePlugins] : [],
|
|
559
|
+
enablePlugins: overrides?.enablePlugins ? [...overrides.enablePlugins] : [],
|
|
560
|
+
envKeys: overrides?.envKeys ? [...overrides.envKeys] : [],
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function defaultCliOptions() {
|
|
564
|
+
return {
|
|
565
|
+
projectRoot: process.cwd(),
|
|
566
|
+
cachePath: "",
|
|
567
|
+
remoteProjectDir: "",
|
|
568
|
+
name: "",
|
|
569
|
+
yes: false,
|
|
570
|
+
dryRun: false,
|
|
571
|
+
disablePlugins: [],
|
|
572
|
+
enablePlugins: [],
|
|
573
|
+
resetPluginPrefs: false,
|
|
574
|
+
listPlugins: false,
|
|
575
|
+
includeAuth: true,
|
|
576
|
+
includeAgentContext: true,
|
|
577
|
+
includeGitDir: true,
|
|
578
|
+
includeAllCopilotWorkspaces: false,
|
|
579
|
+
snapshot: true,
|
|
580
|
+
skipSync: false,
|
|
581
|
+
install: false,
|
|
582
|
+
autoSsh: true,
|
|
583
|
+
envKeys: [],
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function finalizeCliOptions(options) {
|
|
392
587
|
const projectStats = await stat(options.projectRoot).catch(() => null);
|
|
393
588
|
if (!projectStats?.isDirectory()) {
|
|
394
589
|
throw new Error(`project path is not a directory: ${options.projectRoot}`);
|
|
395
590
|
}
|
|
396
591
|
const projectName = sanitizeName(path.basename(options.projectRoot));
|
|
397
|
-
options.
|
|
398
|
-
options.
|
|
399
|
-
|
|
400
|
-
|
|
592
|
+
const remoteProjectDir = options.remoteProjectDir ? normalizeRemotePath(options.remoteProjectDir) : defaultRemoteProjectDir(options.projectRoot);
|
|
593
|
+
const cachePath = options.cachePath ? path.resolve(options.cachePath) : path.join(options.projectRoot, ".freestyle-sync", "cache.json");
|
|
594
|
+
return {
|
|
595
|
+
...options,
|
|
596
|
+
name: options.name || `${CLI_NAME}-${projectName}`,
|
|
597
|
+
remoteProjectDir,
|
|
598
|
+
cachePath,
|
|
599
|
+
};
|
|
401
600
|
}
|
|
402
601
|
function printHelp() {
|
|
403
|
-
console.log(
|
|
602
|
+
console.log(`${CLI_NAME} uploads the current project into a Freestyle VM.
|
|
404
603
|
|
|
405
604
|
Usage:
|
|
406
|
-
|
|
605
|
+
${CLI_NAME} [project-dir] [options]
|
|
407
606
|
|
|
408
607
|
Options:
|
|
409
608
|
--vm-id <id> Sync into an existing Freestyle VM.
|
|
410
|
-
|
|
609
|
+
--name <name> Name for a newly created VM. Defaults to ${CLI_NAME}-<project>.
|
|
411
610
|
--remote-dir <path> Remote project directory. Defaults to the local absolute path.
|
|
412
611
|
--cache <path> Snapshot/hash cache path. Defaults to .freestyle-sync/cache.json.
|
|
413
612
|
--include-env <name> Always copy an environment variable. Repeatable.
|
|
@@ -464,6 +663,18 @@ async function readCache(cachePath, options) {
|
|
|
464
663
|
contextFiles: {},
|
|
465
664
|
};
|
|
466
665
|
}
|
|
666
|
+
function normalizeCache(cache, options) {
|
|
667
|
+
return {
|
|
668
|
+
...cache,
|
|
669
|
+
version: CACHE_VERSION,
|
|
670
|
+
projectRoot: options.projectRoot,
|
|
671
|
+
remoteProjectDir: options.remoteProjectDir,
|
|
672
|
+
projectFiles: cache.projectFiles ?? {},
|
|
673
|
+
contextFiles: cache.contextFiles ?? {},
|
|
674
|
+
snapshotProjectFiles: cache.snapshotProjectFiles ?? {},
|
|
675
|
+
snapshotContextFiles: cache.snapshotContextFiles ?? {},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
467
678
|
function cacheBaseForSync(options, cache) {
|
|
468
679
|
if (options.vmId && options.vmId === cache.vmId) {
|
|
469
680
|
return {
|
|
@@ -632,10 +843,13 @@ async function scanContextCandidates(candidates) {
|
|
|
632
843
|
}
|
|
633
844
|
return entries.sort((left, right) => left.remotePath.localeCompare(right.remotePath));
|
|
634
845
|
}
|
|
635
|
-
async function updatePluginPreferences(options) {
|
|
846
|
+
async function updatePluginPreferences(options, providedPreferences) {
|
|
636
847
|
const preferencesPath = getPluginPreferencesPath(options);
|
|
637
|
-
const
|
|
638
|
-
|
|
848
|
+
const usingProvidedPreferences = Boolean(providedPreferences);
|
|
849
|
+
const preferences = options.resetPluginPrefs
|
|
850
|
+
? emptyPluginPreferences()
|
|
851
|
+
: normalizePluginPreferences(providedPreferences ?? await readPluginPreferences(preferencesPath));
|
|
852
|
+
let changed = options.resetPluginPrefs && !usingProvidedPreferences;
|
|
639
853
|
const disabledPlugins = new Set();
|
|
640
854
|
for (const savedName of preferences.disabledPlugins) {
|
|
641
855
|
const canonicalName = maybeResolvePluginSelector(savedName);
|
|
@@ -656,7 +870,7 @@ async function updatePluginPreferences(options) {
|
|
|
656
870
|
}
|
|
657
871
|
}
|
|
658
872
|
const next = { version: PLUGIN_PREFERENCES_VERSION, disabledPlugins: [...disabledPlugins].sort(), updatedAt: preferences.updatedAt };
|
|
659
|
-
if (changed)
|
|
873
|
+
if (changed && !usingProvidedPreferences)
|
|
660
874
|
await writePluginPreferences(preferencesPath, next);
|
|
661
875
|
return next;
|
|
662
876
|
}
|
|
@@ -719,6 +933,13 @@ function pluginSelectorAliases(name) {
|
|
|
719
933
|
function emptyPluginPreferences() {
|
|
720
934
|
return { version: PLUGIN_PREFERENCES_VERSION, disabledPlugins: [] };
|
|
721
935
|
}
|
|
936
|
+
function normalizePluginPreferences(preferences) {
|
|
937
|
+
return {
|
|
938
|
+
version: PLUGIN_PREFERENCES_VERSION,
|
|
939
|
+
disabledPlugins: Array.isArray(preferences.disabledPlugins) ? preferences.disabledPlugins.filter((name) => typeof name === "string") : [],
|
|
940
|
+
updatedAt: preferences.updatedAt,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
722
943
|
function getPluginPreferencesPath(options) {
|
|
723
944
|
return path.join(path.dirname(options.cachePath), "plugin-preferences.json");
|
|
724
945
|
}
|
|
@@ -726,11 +947,7 @@ async function readPluginPreferences(preferencesPath) {
|
|
|
726
947
|
try {
|
|
727
948
|
const parsed = JSON.parse(await readFile(preferencesPath, "utf8"));
|
|
728
949
|
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
|
-
};
|
|
950
|
+
return normalizePluginPreferences(parsed);
|
|
734
951
|
}
|
|
735
952
|
}
|
|
736
953
|
catch (error) {
|
|
@@ -793,7 +1010,198 @@ function color(code, text) {
|
|
|
793
1010
|
return text;
|
|
794
1011
|
return `\u001b[${code}m${text}\u001b[0m`;
|
|
795
1012
|
}
|
|
1013
|
+
async function getFreestyleClient() {
|
|
1014
|
+
const baseUrl = process.env.FREESTYLE_API_URL || DEFAULT_FREESTYLE_API_URL;
|
|
1015
|
+
if (process.env.FREESTYLE_API_KEY) {
|
|
1016
|
+
return new Freestyle({ apiKey: process.env.FREESTYLE_API_KEY, baseUrl });
|
|
1017
|
+
}
|
|
1018
|
+
const config = resolveStackConfig();
|
|
1019
|
+
let auth = await readStoredFreestyleAuth(config);
|
|
1020
|
+
if (!auth?.refreshToken && !readRefreshTokenFromEnv()) {
|
|
1021
|
+
await runFreestyleLogin();
|
|
1022
|
+
auth = await readStoredFreestyleAuth(config);
|
|
1023
|
+
}
|
|
1024
|
+
let refreshed = await refreshStackAccessToken(config, readRefreshTokenFromEnv() ?? auth?.refreshToken);
|
|
1025
|
+
if (!refreshed) {
|
|
1026
|
+
await runFreestyleLogin(["--force"]);
|
|
1027
|
+
auth = await readStoredFreestyleAuth(config);
|
|
1028
|
+
refreshed = await refreshStackAccessToken(config, readRefreshTokenFromEnv() ?? auth?.refreshToken);
|
|
1029
|
+
}
|
|
1030
|
+
if (!refreshed) {
|
|
1031
|
+
throw new Error("Freestyle authentication failed. Run `npx freestyle login` or set FREESTYLE_API_KEY.");
|
|
1032
|
+
}
|
|
1033
|
+
if (refreshed.refreshToken && refreshed.refreshToken !== auth?.refreshToken) {
|
|
1034
|
+
auth = await updateStoredFreestyleRefreshToken(config, auth, refreshed.refreshToken);
|
|
1035
|
+
}
|
|
1036
|
+
let teamId = process.env.FREESTYLE_TEAM_ID || auth?.defaultTeamId;
|
|
1037
|
+
if (!teamId) {
|
|
1038
|
+
await runFreestyleLogin();
|
|
1039
|
+
auth = await readStoredFreestyleAuth(config);
|
|
1040
|
+
teamId = process.env.FREESTYLE_TEAM_ID || auth?.defaultTeamId;
|
|
1041
|
+
}
|
|
1042
|
+
if (!teamId) {
|
|
1043
|
+
throw new Error("No Freestyle team selected. Run `npx freestyle login` to configure a default team, or set FREESTYLE_TEAM_ID.");
|
|
1044
|
+
}
|
|
1045
|
+
return new Freestyle({
|
|
1046
|
+
apiKey: "placeholder",
|
|
1047
|
+
baseUrl,
|
|
1048
|
+
fetch: createFreestyleProxyFetch(refreshed.accessToken, teamId),
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function resolveStackConfig() {
|
|
1052
|
+
return {
|
|
1053
|
+
stackApiUrl: (process.env.FREESTYLE_STACK_API_URL || DEFAULT_STACK_API_URL).replace(/\/+$/, ""),
|
|
1054
|
+
projectId: process.env.FREESTYLE_STACK_PROJECT_ID || process.env.NEXT_PUBLIC_STACK_PROJECT_ID || process.env.VITE_STACK_PROJECT_ID || DEFAULT_STACK_PROJECT_ID,
|
|
1055
|
+
publishableClientKey: process.env.FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY || process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY || process.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY || DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY,
|
|
1056
|
+
authFilePath: process.env.FREESTYLE_STACK_AUTH_FILE || path.join(homedir(), ".freestyle", "stack-auth.json"),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function readRefreshTokenFromEnv() {
|
|
1060
|
+
const refreshToken = process.env[STACK_REFRESH_TOKEN_ENV_KEY]?.trim();
|
|
1061
|
+
return refreshToken ? refreshToken : undefined;
|
|
1062
|
+
}
|
|
1063
|
+
async function readStoredFreestyleAuth(config) {
|
|
1064
|
+
try {
|
|
1065
|
+
const parsed = JSON.parse(await readFile(config.authFilePath, "utf8"));
|
|
1066
|
+
if (typeof parsed.refreshToken !== "string" || parsed.refreshToken.length === 0)
|
|
1067
|
+
return undefined;
|
|
1068
|
+
return {
|
|
1069
|
+
refreshToken: parsed.refreshToken,
|
|
1070
|
+
defaultTeamId: typeof parsed.defaultTeamId === "string" ? parsed.defaultTeamId : undefined,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
if (error.code === "ENOENT")
|
|
1075
|
+
return undefined;
|
|
1076
|
+
return undefined;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function updateStoredFreestyleRefreshToken(config, auth, refreshToken) {
|
|
1080
|
+
const next = {
|
|
1081
|
+
refreshToken,
|
|
1082
|
+
updatedAt: Date.now(),
|
|
1083
|
+
defaultTeamId: auth?.defaultTeamId,
|
|
1084
|
+
};
|
|
1085
|
+
await mkdir(path.dirname(config.authFilePath), { recursive: true });
|
|
1086
|
+
await writeFile(config.authFilePath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
1087
|
+
return {
|
|
1088
|
+
refreshToken,
|
|
1089
|
+
defaultTeamId: auth?.defaultTeamId,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
async function refreshStackAccessToken(config, refreshToken) {
|
|
1093
|
+
if (!refreshToken)
|
|
1094
|
+
return undefined;
|
|
1095
|
+
const response = await fetch(`${config.stackApiUrl}/api/v1/auth/sessions/current/refresh`, {
|
|
1096
|
+
method: "POST",
|
|
1097
|
+
headers: {
|
|
1098
|
+
...stackClientHeaders(config),
|
|
1099
|
+
"x-stack-refresh-token": refreshToken,
|
|
1100
|
+
},
|
|
1101
|
+
body: "{}",
|
|
1102
|
+
});
|
|
1103
|
+
if (!response.ok)
|
|
1104
|
+
return undefined;
|
|
1105
|
+
const data = await response.json();
|
|
1106
|
+
if (typeof data.access_token !== "string")
|
|
1107
|
+
return undefined;
|
|
1108
|
+
return {
|
|
1109
|
+
accessToken: data.access_token,
|
|
1110
|
+
refreshToken: typeof data.refresh_token === "string" ? data.refresh_token : undefined,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function stackClientHeaders(config) {
|
|
1114
|
+
return {
|
|
1115
|
+
"Content-Type": "application/json",
|
|
1116
|
+
"x-stack-project-id": config.projectId,
|
|
1117
|
+
"x-stack-access-type": "client",
|
|
1118
|
+
"x-stack-publishable-client-key": config.publishableClientKey,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function createFreestyleProxyFetch(accessToken, teamId) {
|
|
1122
|
+
const dashboardApiUrl = (process.env.FREESTYLE_DASHBOARD_URL || "https://dash.freestyle.sh").replace(/\/+$/, "");
|
|
1123
|
+
return async (url, init) => {
|
|
1124
|
+
const urlObject = typeof url === "string" ? new URL(url) : url instanceof URL ? url : new URL(url.url);
|
|
1125
|
+
const requestPath = `${urlObject.pathname}${urlObject.search}`;
|
|
1126
|
+
const proxyResponse = await fetch(`${dashboardApiUrl}/api/proxy/request`, {
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
headers: { "Content-Type": "application/json" },
|
|
1129
|
+
body: JSON.stringify({
|
|
1130
|
+
data: {
|
|
1131
|
+
accessToken,
|
|
1132
|
+
teamId,
|
|
1133
|
+
path: requestPath.startsWith("/") ? requestPath.slice(1) : requestPath,
|
|
1134
|
+
method: init?.method || "GET",
|
|
1135
|
+
headers: init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {},
|
|
1136
|
+
body: init?.body ? init.body.toString() : undefined,
|
|
1137
|
+
},
|
|
1138
|
+
}),
|
|
1139
|
+
});
|
|
1140
|
+
if (!proxyResponse.ok) {
|
|
1141
|
+
const body = await proxyResponse.text();
|
|
1142
|
+
const normalizedError = normalizeFreestyleProxyError(body, proxyResponse.status);
|
|
1143
|
+
return new Response(normalizedError.body, {
|
|
1144
|
+
status: proxyResponse.status,
|
|
1145
|
+
statusText: proxyResponse.statusText,
|
|
1146
|
+
headers: { "Content-Type": normalizedError.contentType },
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
const data = await proxyResponse.json();
|
|
1150
|
+
return new Response(JSON.stringify(data), {
|
|
1151
|
+
status: 200,
|
|
1152
|
+
headers: { "Content-Type": "application/json" },
|
|
1153
|
+
});
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
function normalizeFreestyleProxyError(errorText, status) {
|
|
1157
|
+
const fallbackCode = status === 400 ? "BAD_REQUEST" : status === 401 ? "UNAUTHORIZED_ERROR" : status === 403 ? "FORBIDDEN" : "INTERNAL_ERROR";
|
|
1158
|
+
try {
|
|
1159
|
+
const parsed = JSON.parse(errorText);
|
|
1160
|
+
if (typeof parsed.code === "string" && typeof parsed.message === "string") {
|
|
1161
|
+
return { body: JSON.stringify(parsed), contentType: "application/json" };
|
|
1162
|
+
}
|
|
1163
|
+
const message = [parsed.error, parsed.message, parsed.reason].find((value) => typeof value === "string" && value.length > 0);
|
|
1164
|
+
if (message) {
|
|
1165
|
+
return {
|
|
1166
|
+
body: JSON.stringify(freestyleErrorBody(fallbackCode, message)),
|
|
1167
|
+
contentType: "application/json",
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
body: JSON.stringify(freestyleErrorBody(fallbackCode, errorText || "Request failed")),
|
|
1175
|
+
contentType: "application/json",
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function freestyleErrorBody(code, message) {
|
|
1179
|
+
if (code === "UNAUTHORIZED_ERROR") {
|
|
1180
|
+
return { code, message, route: "/api/proxy/request", reason: message };
|
|
1181
|
+
}
|
|
1182
|
+
return { code, message };
|
|
1183
|
+
}
|
|
1184
|
+
async function runFreestyleLogin(extraArgs = []) {
|
|
1185
|
+
console.log("No Freestyle API key found. Opening Freestyle login...");
|
|
1186
|
+
const cliPath = path.join(path.dirname(fileURLToPath(import.meta.resolve("freestyle"))), "cli.mjs");
|
|
1187
|
+
await new Promise((resolve, reject) => {
|
|
1188
|
+
const child = spawn(process.execPath, [cliPath, "login", ...extraArgs], {
|
|
1189
|
+
cwd: process.cwd(),
|
|
1190
|
+
stdio: "inherit",
|
|
1191
|
+
env: process.env,
|
|
1192
|
+
});
|
|
1193
|
+
child.on("error", reject);
|
|
1194
|
+
child.on("exit", (code, signal) => {
|
|
1195
|
+
if (code === 0) {
|
|
1196
|
+
resolve();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
reject(new Error(signal ? `freestyle login was interrupted by ${signal}` : `freestyle login failed with exit code ${code ?? "unknown"}`));
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
796
1203
|
async function getOrCreateVm(options, snapshotId) {
|
|
1204
|
+
const freestyle = await getFreestyleClient();
|
|
797
1205
|
if (options.vmId) {
|
|
798
1206
|
const { vm } = await freestyle.vms.get({ vmId: options.vmId });
|
|
799
1207
|
await vm.start(options.idleTimeoutSeconds ? { idleTimeoutSeconds: options.idleTimeoutSeconds } : undefined);
|
|
@@ -815,8 +1223,8 @@ async function syncProject(vm, vmId, options, changes) {
|
|
|
815
1223
|
console.log(`VM ${vmId}: uploading ${changes.changed.length} changed project files...`);
|
|
816
1224
|
const archive = await createProjectArchive(options.projectRoot, changes.changed);
|
|
817
1225
|
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/
|
|
1226
|
+
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-project.tgz", "project");
|
|
1227
|
+
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
1228
|
}
|
|
821
1229
|
finally {
|
|
822
1230
|
await rm(path.dirname(archive), { recursive: true, force: true });
|
|
@@ -825,8 +1233,8 @@ async function syncProject(vm, vmId, options, changes) {
|
|
|
825
1233
|
if (changes.removed.length > 0) {
|
|
826
1234
|
console.log(`VM ${vmId}: removing ${changes.removed.length} deleted project files...`);
|
|
827
1235
|
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/
|
|
1236
|
+
await vm.fs.writeFile("/tmp/freestyle-sync-remove-list", removeList);
|
|
1237
|
+
await checkedExec(vm, `cd ${shellQuote(options.remoteProjectDir)} && xargs -0 rm -f -- < /tmp/freestyle-sync-remove-list && rm -f /tmp/freestyle-sync-remove-list`);
|
|
830
1238
|
}
|
|
831
1239
|
return {
|
|
832
1240
|
uploaded: changes.changed.length,
|
|
@@ -885,8 +1293,8 @@ async function syncContext(vm, vmId, changes) {
|
|
|
885
1293
|
console.log(`VM ${vmId}: uploading ${changes.changed.length} changed auth/context files...`);
|
|
886
1294
|
const archive = await createContextArchive(changes.changed);
|
|
887
1295
|
try {
|
|
888
|
-
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/
|
|
889
|
-
await checkedExec(vm, "tar --no-same-owner --no-same-permissions -xzf /tmp/
|
|
1296
|
+
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-context.tgz", "context");
|
|
1297
|
+
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
1298
|
}
|
|
891
1299
|
finally {
|
|
892
1300
|
await rm(path.dirname(archive), { recursive: true, force: true });
|
|
@@ -954,7 +1362,7 @@ async function detectInstallCommand(projectRoot) {
|
|
|
954
1362
|
return undefined;
|
|
955
1363
|
}
|
|
956
1364
|
async function createProjectArchive(projectRoot, entries) {
|
|
957
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "
|
|
1365
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-"));
|
|
958
1366
|
const listPath = path.join(tempDir, "files.list");
|
|
959
1367
|
const archivePath = path.join(tempDir, "project.tgz");
|
|
960
1368
|
await writeFile(listPath, Buffer.from(entries.map((entry) => entry.relativePath).join("\0") + "\0"));
|
|
@@ -962,7 +1370,7 @@ async function createProjectArchive(projectRoot, entries) {
|
|
|
962
1370
|
return archivePath;
|
|
963
1371
|
}
|
|
964
1372
|
async function createContextArchive(entries) {
|
|
965
|
-
const tempDir = await mkdtemp(path.join(tmpdir(), "
|
|
1373
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-context-"));
|
|
966
1374
|
const stagingDir = path.join(tempDir, "staging");
|
|
967
1375
|
const archivePath = path.join(tempDir, "context.tgz");
|
|
968
1376
|
await mkdir(stagingDir, { recursive: true });
|
|
@@ -987,17 +1395,29 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
987
1395
|
const archive = await readFile(archivePath);
|
|
988
1396
|
const encoded = archive.toString("base64");
|
|
989
1397
|
const chunkCount = Math.max(1, Math.ceil(encoded.length / ARCHIVE_CHUNK_CHARS));
|
|
990
|
-
const chunkDir = `/tmp/
|
|
1398
|
+
const chunkDir = `/tmp/freestyle-sync-${label}-${Date.now()}.chunks`;
|
|
991
1399
|
console.log(`VM ${vmId}: streaming ${formatBytes(archive.length)} ${label} archive in ${chunkCount} chunk${chunkCount === 1 ? "" : "s"}...`);
|
|
992
1400
|
await checkedExec(vm, `rm -rf ${shellQuote(chunkDir)} && mkdir -p ${shellQuote(chunkDir)}`);
|
|
993
1401
|
const width = String(chunkCount - 1).length;
|
|
1402
|
+
const canRenderInlineProgress = process.stdout.isTTY;
|
|
1403
|
+
const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
|
|
994
1404
|
for (let index = 0; index < chunkCount; index += 1) {
|
|
995
1405
|
const start = index * ARCHIVE_CHUNK_CHARS;
|
|
996
1406
|
const chunk = encoded.slice(start, start + ARCHIVE_CHUNK_CHARS);
|
|
997
1407
|
const chunkName = `${String(index).padStart(width, "0")}.b64`;
|
|
998
1408
|
await vm.fs.writeTextFile(`${chunkDir}/${chunkName}`, chunk);
|
|
999
|
-
|
|
1000
|
-
|
|
1409
|
+
const uploadedChunks = index + 1;
|
|
1410
|
+
if (chunkCount > 1) {
|
|
1411
|
+
const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
|
|
1412
|
+
if (canRenderInlineProgress) {
|
|
1413
|
+
process.stdout.write(`\r${progressMessage}`);
|
|
1414
|
+
if (uploadedChunks === chunkCount) {
|
|
1415
|
+
process.stdout.write("\n");
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
else if (uploadedChunks === 1 || uploadedChunks % logEvery === 0 || uploadedChunks === chunkCount) {
|
|
1419
|
+
console.log(progressMessage);
|
|
1420
|
+
}
|
|
1001
1421
|
}
|
|
1002
1422
|
}
|
|
1003
1423
|
await checkedExec(vm, `cat ${shellQuote(chunkDir)}/*.b64 | base64 -d > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`);
|
|
@@ -1095,7 +1515,7 @@ function md5(value) {
|
|
|
1095
1515
|
return createHash("md5").update(value).digest("hex");
|
|
1096
1516
|
}
|
|
1097
1517
|
function renderEnvFile(envExports) {
|
|
1098
|
-
const lines = [
|
|
1518
|
+
const lines = [`# Generated by ${CLI_NAME}. Contains local auth tokens.`];
|
|
1099
1519
|
for (const key of Object.keys(envExports).sort()) {
|
|
1100
1520
|
lines.push(`export ${key}=${shellQuote(envExports[key])}`);
|
|
1101
1521
|
}
|
|
@@ -1115,7 +1535,28 @@ function defaultRemoteProjectDir(projectRoot) {
|
|
|
1115
1535
|
return normalizeRemotePath(toPosix(projectRoot));
|
|
1116
1536
|
}
|
|
1117
1537
|
function sanitizeName(value) {
|
|
1118
|
-
|
|
1538
|
+
const lower = value.toLowerCase();
|
|
1539
|
+
let normalized = "";
|
|
1540
|
+
let lastWasDash = false;
|
|
1541
|
+
for (const character of lower) {
|
|
1542
|
+
const isAlpha = character >= "a" && character <= "z";
|
|
1543
|
+
const isDigit = character >= "0" && character <= "9";
|
|
1544
|
+
const isAllowedPunctuation = character === "." || character === "_" || character === "-";
|
|
1545
|
+
if (isAlpha || isDigit || isAllowedPunctuation) {
|
|
1546
|
+
normalized += character;
|
|
1547
|
+
lastWasDash = false;
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (!lastWasDash) {
|
|
1551
|
+
normalized += "-";
|
|
1552
|
+
lastWasDash = true;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
while (normalized.startsWith("-"))
|
|
1556
|
+
normalized = normalized.slice(1);
|
|
1557
|
+
while (normalized.endsWith("-"))
|
|
1558
|
+
normalized = normalized.slice(0, -1);
|
|
1559
|
+
return normalized || "project";
|
|
1119
1560
|
}
|
|
1120
1561
|
function toPosix(value) {
|
|
1121
1562
|
return value.split(path.sep).join(path.posix.sep);
|