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