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