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