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,9 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { definePlugin } from "../../../src/plugin-api.
|
|
4
|
-
|
|
3
|
+
import { definePlugin } from "../../../src/plugin-api.js";
|
|
5
4
|
const execFileAsync = promisify(execFile);
|
|
6
|
-
|
|
7
5
|
export function nodeNpmPlugin() {
|
|
8
6
|
return definePlugin({
|
|
9
7
|
name: "@freestyle-sync/node-npm",
|
|
@@ -370,21 +368,21 @@ cp "$fingerprint_file" "$cache_file"
|
|
|
370
368
|
if (result.statusCode && result.statusCode !== 0) {
|
|
371
369
|
throw new Error(result.stderr ?? result.stdout ?? `Node/npm fixup failed with status ${result.statusCode}`);
|
|
372
370
|
}
|
|
373
|
-
if (result.stdout?.trim())
|
|
371
|
+
if (result.stdout?.trim())
|
|
372
|
+
console.log(result.stdout.trim());
|
|
374
373
|
},
|
|
375
374
|
});
|
|
376
375
|
}
|
|
377
|
-
|
|
378
376
|
async function localNpmVersion() {
|
|
379
377
|
try {
|
|
380
378
|
const { stdout } = await execFileAsync("npm", ["--version"]);
|
|
381
379
|
const version = stdout.trim();
|
|
382
380
|
return version.length > 0 ? version : undefined;
|
|
383
|
-
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
384
383
|
return undefined;
|
|
385
384
|
}
|
|
386
385
|
}
|
|
387
|
-
|
|
388
|
-
function shellQuote(value: string): string {
|
|
386
|
+
function shellQuote(value) {
|
|
389
387
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
390
388
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export const npmNativeDepsPlugin = {
|
|
5
|
+
name: "npm-native-deps",
|
|
6
|
+
async afterProjectSync({ vm, options }) {
|
|
7
|
+
const nodeVersion = process.versions.node;
|
|
8
|
+
const npmVersion = await localNpmVersion();
|
|
9
|
+
const script = `
|
|
10
|
+
set -eu
|
|
11
|
+
cd ${shellQuote(options.remoteProjectDir)}
|
|
12
|
+
if [ ! -f package.json ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
desired_node=${shellQuote(nodeVersion)}
|
|
16
|
+
desired_npm=${shellQuote(npmVersion ?? "")}
|
|
17
|
+
nvm_version=v0.40.3
|
|
18
|
+
export NVM_DIR=/root/.nvm
|
|
19
|
+
runtime_cache=.vmpush/node-runtime-cache.json
|
|
20
|
+
|
|
21
|
+
install_nvm() {
|
|
22
|
+
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
mkdir -p "$NVM_DIR"
|
|
26
|
+
if command -v git >/dev/null 2>&1; then
|
|
27
|
+
rm -rf "$NVM_DIR"
|
|
28
|
+
git clone --depth 1 --branch "$nvm_version" https://github.com/nvm-sh/nvm.git "$NVM_DIR"
|
|
29
|
+
elif command -v curl >/dev/null 2>&1; then
|
|
30
|
+
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh" | PROFILE=/dev/null bash
|
|
31
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
32
|
+
wget -qO- "https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh" | PROFILE=/dev/null bash
|
|
33
|
+
else
|
|
34
|
+
echo "git, curl, or wget is required to install nvm" >&2
|
|
35
|
+
return 1
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
install_nvm
|
|
40
|
+
. "$NVM_DIR/nvm.sh"
|
|
41
|
+
installed_node=$(nvm version "$desired_node" || true)
|
|
42
|
+
if [ "$installed_node" = "N/A" ]; then
|
|
43
|
+
echo "Installing Node $desired_node with nvm"
|
|
44
|
+
nvm install "$desired_node"
|
|
45
|
+
fi
|
|
46
|
+
nvm alias default "$desired_node" >/dev/null
|
|
47
|
+
nvm use --silent "$desired_node"
|
|
48
|
+
actual_node=$(node -p 'process.versions.node')
|
|
49
|
+
if [ "$actual_node" != "$desired_node" ]; then
|
|
50
|
+
echo "Expected Node $desired_node but activated $actual_node" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
actual_npm=$(npm --version 2>/dev/null || true)
|
|
54
|
+
if [ -n "$desired_npm" ] && [ "$actual_npm" != "$desired_npm" ]; then
|
|
55
|
+
echo "Installing npm $desired_npm for Node $desired_node"
|
|
56
|
+
npm install -g "npm@$desired_npm"
|
|
57
|
+
actual_npm=$(npm --version)
|
|
58
|
+
fi
|
|
59
|
+
mkdir -p .vmpush
|
|
60
|
+
printf '{"node":"%s","npm":"%s","nvm":"%s"}\n' "$actual_node" "$actual_npm" "$nvm_version" > "$runtime_cache"
|
|
61
|
+
grep -qxF 'export NVM_DIR=/root/.nvm' /root/.profile || printf '\n%s\n' 'export NVM_DIR=/root/.nvm' >> /root/.profile
|
|
62
|
+
grep -qxF '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use --silent default >/dev/null 2>&1' /root/.profile || printf '%s\n' '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use --silent default >/dev/null 2>&1' >> /root/.profile
|
|
63
|
+
|
|
64
|
+
if [ ! -d node_modules ]; then
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
package_list=$(mktemp /tmp/vmpush-native-packages.XXXXXX)
|
|
68
|
+
fingerprint_file=$(mktemp /tmp/vmpush-native-fingerprints.XXXXXX)
|
|
69
|
+
scanner_file=$(mktemp /tmp/vmpush-native-scanner.XXXXXX.js)
|
|
70
|
+
cache_file=.vmpush/npm-native-deps-cache.json
|
|
71
|
+
trap 'rm -f "$package_list" "$fingerprint_file" "$scanner_file"' EXIT
|
|
72
|
+
cat > "$scanner_file" <<'NODE'
|
|
73
|
+
const fs = require('node:fs');
|
|
74
|
+
const path = require('node:path');
|
|
75
|
+
const crypto = require('node:crypto');
|
|
76
|
+
|
|
77
|
+
const cachePath = path.resolve(process.argv[2]);
|
|
78
|
+
const fingerprintPath = process.argv[3];
|
|
79
|
+
const mode = process.argv[4];
|
|
80
|
+
const nodeModules = path.resolve('node_modules');
|
|
81
|
+
const packages = new Set();
|
|
82
|
+
const packageInfos = [];
|
|
83
|
+
const incompatiblePlatformPackages = new Set();
|
|
84
|
+
const nativeFilesByPackage = new Map();
|
|
85
|
+
const platformPackagesByWrapper = new Map();
|
|
86
|
+
|
|
87
|
+
function readPackageJson(packageDir) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function addPackage(packageDir) {
|
|
96
|
+
const packageJson = readPackageJson(packageDir);
|
|
97
|
+
if (typeof packageJson?.name !== 'string' || packageJson.name.length === 0) return;
|
|
98
|
+
packageInfos.push({ dir: packageDir, json: packageJson, name: packageJson.name });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function listAllowsCurrent(value, current) {
|
|
102
|
+
if (value === undefined) return true;
|
|
103
|
+
const list = Array.isArray(value) ? value : [value];
|
|
104
|
+
const positives = list.filter((item) => typeof item === 'string' && !item.startsWith('!'));
|
|
105
|
+
const negatives = list.filter((item) => typeof item === 'string' && item.startsWith('!')).map((item) => item.slice(1));
|
|
106
|
+
if (negatives.includes(current)) return false;
|
|
107
|
+
return positives.length === 0 || positives.includes(current);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function packageAllowsCurrentPlatform(packageJson) {
|
|
111
|
+
return listAllowsCurrent(packageJson.os, process.platform) && listAllowsCurrent(packageJson.cpu, process.arch);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dependencyNames(packageJson) {
|
|
115
|
+
return Object.keys(Object.assign({}, packageJson.dependencies, packageJson.optionalDependencies));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addMapSet(map, key, value) {
|
|
119
|
+
const existing = map.get(key);
|
|
120
|
+
if (existing) existing.add(value);
|
|
121
|
+
else map.set(key, new Set([value]));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function relativeToProject(filePath) {
|
|
125
|
+
return path.relative(process.cwd(), filePath).split(path.sep).join('/');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectPackageFiles(packageDir, files) {
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = fs.readdirSync(packageDir, { withFileTypes: true });
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const child = path.join(packageDir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) collectPackageFiles(child, files);
|
|
139
|
+
else if (entry.isFile()) files.push(child);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function packageFingerprint(packageName) {
|
|
144
|
+
const info = packageInfos.find((candidate) => candidate.name === packageName);
|
|
145
|
+
if (!info) return undefined;
|
|
146
|
+
const files = [path.join(info.dir, 'package.json')];
|
|
147
|
+
for (const nativeFile of nativeFilesByPackage.get(packageName) || []) files.push(nativeFile);
|
|
148
|
+
for (const platformPackageName of platformPackagesByWrapper.get(packageName) || []) {
|
|
149
|
+
const platformInfo = packageInfos.find((candidate) => candidate.name === platformPackageName);
|
|
150
|
+
if (!platformInfo) continue;
|
|
151
|
+
files.push(path.join(platformInfo.dir, 'package.json'));
|
|
152
|
+
collectPackageFiles(platformInfo.dir, files);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hash = crypto.createHash('sha256');
|
|
156
|
+
hash.update(process.platform);
|
|
157
|
+
hash.update('\0');
|
|
158
|
+
hash.update(process.arch);
|
|
159
|
+
hash.update('\0');
|
|
160
|
+
hash.update(process.versions.modules || '');
|
|
161
|
+
hash.update('\0');
|
|
162
|
+
for (const filePath of Array.from(new Set(files)).sort()) {
|
|
163
|
+
try {
|
|
164
|
+
const stats = fs.statSync(filePath);
|
|
165
|
+
if (!stats.isFile()) continue;
|
|
166
|
+
hash.update(relativeToProject(filePath));
|
|
167
|
+
hash.update('\0');
|
|
168
|
+
hash.update(String(stats.size));
|
|
169
|
+
hash.update('\0');
|
|
170
|
+
hash.update(String(stats.mtimeMs));
|
|
171
|
+
hash.update('\0');
|
|
172
|
+
hash.update(String(stats.mode));
|
|
173
|
+
hash.update('\0');
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore files that disappeared while scanning.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return hash.digest('hex');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function nearestPackageDir(start) {
|
|
182
|
+
let current = start;
|
|
183
|
+
while (current.startsWith(nodeModules)) {
|
|
184
|
+
if (fs.existsSync(path.join(current, 'package.json'))) return current;
|
|
185
|
+
const parent = path.dirname(current);
|
|
186
|
+
if (parent === current) break;
|
|
187
|
+
current = parent;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addNativePackage(nativeFile) {
|
|
193
|
+
const packageDir = nearestPackageDir(path.dirname(nativeFile));
|
|
194
|
+
if (!packageDir) return;
|
|
195
|
+
const packageJson = readPackageJson(packageDir);
|
|
196
|
+
if (typeof packageJson?.name !== 'string' || packageJson.name.length === 0) return;
|
|
197
|
+
packages.add(packageJson.name);
|
|
198
|
+
addMapSet(nativeFilesByPackage, packageJson.name, nativeFile);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function walk(directory) {
|
|
202
|
+
let entries;
|
|
203
|
+
try {
|
|
204
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
205
|
+
} catch {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const child = path.join(directory, entry.name);
|
|
211
|
+
if (entry.isDirectory()) {
|
|
212
|
+
if (entry.name !== 'node_modules' && fs.existsSync(path.join(child, 'package.json'))) addPackage(child);
|
|
213
|
+
walk(child);
|
|
214
|
+
} else if (entry.isFile() && entry.name.endsWith('.node')) {
|
|
215
|
+
addNativePackage(child);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function cacheEntryMatches(entry, fingerprint) {
|
|
221
|
+
if (entry === fingerprint) return true;
|
|
222
|
+
if (entry && typeof entry === 'object' && (entry.input === fingerprint || entry.fixed === fingerprint)) return true;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
walk(nodeModules);
|
|
227
|
+
for (const info of packageInfos) {
|
|
228
|
+
if ((info.json.os !== undefined || info.json.cpu !== undefined) && !packageAllowsCurrentPlatform(info.json)) {
|
|
229
|
+
incompatiblePlatformPackages.add(info.name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const info of packageInfos) {
|
|
233
|
+
const platformDependencies = dependencyNames(info.json).filter((dependency) => incompatiblePlatformPackages.has(dependency));
|
|
234
|
+
if (platformDependencies.length === 0) continue;
|
|
235
|
+
packages.add(info.name);
|
|
236
|
+
for (const dependency of platformDependencies) addMapSet(platformPackagesByWrapper, info.name, dependency);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let previous = {};
|
|
240
|
+
try {
|
|
241
|
+
previous = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
242
|
+
} catch {
|
|
243
|
+
previous = {};
|
|
244
|
+
}
|
|
245
|
+
const next = Object.assign({}, previous);
|
|
246
|
+
const changedPackages = [];
|
|
247
|
+
for (const packageName of Array.from(packages).sort()) {
|
|
248
|
+
const fingerprint = packageFingerprint(packageName);
|
|
249
|
+
if (!fingerprint) continue;
|
|
250
|
+
const previousEntry = previous[packageName];
|
|
251
|
+
if (mode !== 'record' && !cacheEntryMatches(previousEntry, fingerprint)) changedPackages.push(packageName);
|
|
252
|
+
const previousObject = previousEntry && typeof previousEntry === 'object' ? previousEntry : {};
|
|
253
|
+
next[packageName] = mode === 'record'
|
|
254
|
+
? Object.assign({}, previousObject, { fixed: fingerprint })
|
|
255
|
+
: Object.assign({}, previousObject, { input: fingerprint });
|
|
256
|
+
}
|
|
257
|
+
fs.mkdirSync(path.dirname(fingerprintPath), { recursive: true });
|
|
258
|
+
fs.writeFileSync(fingerprintPath, JSON.stringify(next, null, 2) + '\n');
|
|
259
|
+
if (mode !== 'record') {
|
|
260
|
+
for (const packageName of changedPackages) console.log(packageName);
|
|
261
|
+
}
|
|
262
|
+
NODE
|
|
263
|
+
node "$scanner_file" "$cache_file" "$fingerprint_file" detect > "$package_list"
|
|
264
|
+
if [ ! -s "$package_list" ]; then
|
|
265
|
+
mkdir -p .vmpush
|
|
266
|
+
cp "$fingerprint_file" "$cache_file"
|
|
267
|
+
exit 0
|
|
268
|
+
fi
|
|
269
|
+
echo "Rebuilding native Node dependencies: $(tr '\n' ' ' < "$package_list")"
|
|
270
|
+
if [ -f pnpm-lock.yaml ] && command -v corepack >/dev/null 2>&1; then
|
|
271
|
+
corepack enable >/dev/null 2>&1 || true
|
|
272
|
+
xargs pnpm rebuild < "$package_list"
|
|
273
|
+
elif [ -f yarn.lock ] && command -v corepack >/dev/null 2>&1; then
|
|
274
|
+
corepack enable >/dev/null 2>&1 || true
|
|
275
|
+
if yarn rebuild --help >/dev/null 2>&1; then
|
|
276
|
+
xargs yarn rebuild < "$package_list"
|
|
277
|
+
else
|
|
278
|
+
echo "Yarn does not support targeted rebuild here; skipping broad reinstall"
|
|
279
|
+
fi
|
|
280
|
+
elif command -v npm >/dev/null 2>&1; then
|
|
281
|
+
xargs npm rebuild < "$package_list"
|
|
282
|
+
fi
|
|
283
|
+
node "$scanner_file" "$cache_file" "$fingerprint_file" record > /dev/null
|
|
284
|
+
mkdir -p .vmpush
|
|
285
|
+
cp "$fingerprint_file" "$cache_file"
|
|
286
|
+
`;
|
|
287
|
+
const result = await vm.exec({ command: script, timeoutMs: 20 * 60 * 1000 });
|
|
288
|
+
if (result.statusCode && result.statusCode !== 0) {
|
|
289
|
+
throw new Error(result.stderr ?? result.stdout ?? `npm native dependency fixup failed with status ${result.statusCode}`);
|
|
290
|
+
}
|
|
291
|
+
if (result.stdout?.trim())
|
|
292
|
+
console.log(result.stdout.trim());
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
async function localNpmVersion() {
|
|
296
|
+
try {
|
|
297
|
+
const { stdout } = await execFileAsync("npm", ["--version"]);
|
|
298
|
+
const version = stdout.trim();
|
|
299
|
+
return version.length > 0 ? version : undefined;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function shellQuote(value) {
|
|
306
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
307
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { definePlugin
|
|
5
|
-
|
|
4
|
+
import { definePlugin } from "../../../src/plugin-api.js";
|
|
6
5
|
export function shellHistoryPlugin() {
|
|
7
6
|
return definePlugin({
|
|
8
7
|
name: "@freestyle-sync/shell-history",
|
|
9
8
|
async discoverContextCandidates() {
|
|
10
|
-
const item
|
|
9
|
+
const item = {
|
|
11
10
|
source: path.join(homedir(), ".zsh_history"),
|
|
12
11
|
remoteRoot: "/root/.zsh_history",
|
|
13
12
|
label: "zsh history",
|
|
@@ -18,7 +17,8 @@ export function shellHistoryPlugin() {
|
|
|
18
17
|
return await exists(item.source) ? [item] : [];
|
|
19
18
|
},
|
|
20
19
|
async afterContextSync({ vm, changedRemotePaths, utils }) {
|
|
21
|
-
if (!changedRemotePaths.includes("/root/.zsh_history"))
|
|
20
|
+
if (!changedRemotePaths.includes("/root/.zsh_history"))
|
|
21
|
+
return;
|
|
22
22
|
await seedRemoteShellHistory(vm, utils.checkedExec);
|
|
23
23
|
},
|
|
24
24
|
async afterSync({ vm, utils }) {
|
|
@@ -26,11 +26,7 @@ export function shellHistoryPlugin() {
|
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
async function seedRemoteShellHistory(
|
|
31
|
-
vm: RemoteVm,
|
|
32
|
-
checkedExec: PushvmPluginUtils["checkedExec"],
|
|
33
|
-
) {
|
|
29
|
+
async function seedRemoteShellHistory(vm, checkedExec) {
|
|
34
30
|
await checkedExec(vm, `
|
|
35
31
|
set -eu
|
|
36
32
|
if [ ! -f /root/.zsh_history ]; then
|
|
@@ -58,7 +54,12 @@ chown root:root /root/.zsh_history /root/.bash_history 2>/dev/null || true
|
|
|
58
54
|
chmod 600 /root/.zsh_history /root/.bash_history 2>/dev/null || true
|
|
59
55
|
`);
|
|
60
56
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
async function exists(filePath) {
|
|
58
|
+
try {
|
|
59
|
+
await stat(filePath);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
64
65
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { freestyle } from "freestyle";
|
|
4
|
-
import { definePlugin
|
|
5
|
-
|
|
4
|
+
import { definePlugin } from "../../../src/plugin-api.js";
|
|
6
5
|
const execFileAsync = promisify(execFile);
|
|
7
|
-
|
|
8
6
|
export function vscodePlugin() {
|
|
9
7
|
return definePlugin({
|
|
10
8
|
name: "@freestyle-sync/vscode",
|
|
@@ -15,21 +13,17 @@ export function vscodePlugin() {
|
|
|
15
13
|
},
|
|
16
14
|
async connect(context) {
|
|
17
15
|
const editorScheme = await detectEditorScheme();
|
|
18
|
-
if (!editorScheme)
|
|
16
|
+
if (!editorScheme)
|
|
17
|
+
return false;
|
|
19
18
|
await openRemoteEditor(context, editorScheme);
|
|
20
19
|
return true;
|
|
21
20
|
},
|
|
22
21
|
});
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
async function warmVsCodeServerCache(
|
|
26
|
-
vm: RemoteVm,
|
|
27
|
-
shellQuote: (value: string) => string,
|
|
28
|
-
checkedExec: (vm: RemoteVm, command: string, timeoutMs?: number) => Promise<{ stdout?: string | null }>,
|
|
29
|
-
) {
|
|
23
|
+
async function warmVsCodeServerCache(vm, shellQuote, checkedExec) {
|
|
30
24
|
const commit = await localVsCodeCommit();
|
|
31
|
-
if (!commit)
|
|
32
|
-
|
|
25
|
+
if (!commit)
|
|
26
|
+
return;
|
|
33
27
|
const script = `
|
|
34
28
|
set -eu
|
|
35
29
|
commit=${shellQuote(commit)}
|
|
@@ -75,22 +69,21 @@ if [ ! -x "$cli_server_dir/bin/code-server" ]; then
|
|
|
75
69
|
fi
|
|
76
70
|
echo "Cached VS Code Server $commit for linux-$arch"
|
|
77
71
|
`;
|
|
78
|
-
|
|
79
72
|
const result = await checkedExec(vm, script);
|
|
80
|
-
if (result.stdout?.trim())
|
|
73
|
+
if (result.stdout?.trim())
|
|
74
|
+
console.log(result.stdout.trim());
|
|
81
75
|
}
|
|
82
|
-
|
|
83
76
|
async function localVsCodeCommit() {
|
|
84
77
|
try {
|
|
85
78
|
const { stdout } = await execFileAsync("code", ["--version"]);
|
|
86
79
|
const commit = stdout.split(/\r?\n/)[1]?.trim();
|
|
87
80
|
return /^[0-9a-f]{40}$/i.test(commit ?? "") ? commit : undefined;
|
|
88
|
-
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
89
83
|
return undefined;
|
|
90
84
|
}
|
|
91
85
|
}
|
|
92
|
-
|
|
93
|
-
async function openRemoteEditor(context: ConnectHookContext, scheme: "vscode" | "cursor") {
|
|
86
|
+
async function openRemoteEditor(context, scheme) {
|
|
94
87
|
const { vmId, options } = context;
|
|
95
88
|
console.log(`Opening ${scheme === "cursor" ? "Cursor" : "VS Code"} Remote SSH window for VM ${vmId}...`);
|
|
96
89
|
const { identity, identityId } = await freestyle.identities.create();
|
|
@@ -100,63 +93,68 @@ async function openRemoteEditor(context: ConnectHookContext, scheme: "vscode" |
|
|
|
100
93
|
const uri = `${scheme}://vscode-remote/ssh-remote+${vmId},${token}@${vmId}.vm-ssh.freestyle.sh${encodeRemotePath(options.remoteProjectDir)}?windowId=_blank`;
|
|
101
94
|
try {
|
|
102
95
|
const preOpenMessages = await context.runBeforeOpenRemoteEditor({ scheme, remoteWorkspaceUri });
|
|
103
|
-
for (const message of preOpenMessages)
|
|
96
|
+
for (const message of preOpenMessages)
|
|
97
|
+
console.log(message);
|
|
104
98
|
await openUri(uri);
|
|
105
|
-
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
106
101
|
await identity.tokens.revoke({ tokenId }).catch(() => undefined);
|
|
107
102
|
await freestyle.identities.delete({ identityId }).catch(() => undefined);
|
|
108
103
|
throw error;
|
|
109
104
|
}
|
|
110
105
|
await context.utils.delay(5000);
|
|
111
106
|
const postOpenMessages = await context.runAfterOpenRemoteEditor({ scheme, remoteWorkspaceUri });
|
|
112
|
-
for (const message of postOpenMessages)
|
|
107
|
+
for (const message of postOpenMessages)
|
|
108
|
+
console.log(message);
|
|
113
109
|
console.log(`Opened ${scheme === "cursor" ? "Cursor" : "VS Code"}. SSH identity ${identityId} remains active for the editor session.`);
|
|
114
110
|
}
|
|
115
|
-
|
|
116
|
-
async function detectEditorScheme(): Promise<"vscode" | "cursor" | undefined> {
|
|
111
|
+
async function detectEditorScheme() {
|
|
117
112
|
const envText = Object.entries(process.env)
|
|
118
113
|
.filter(([key]) => key.startsWith("VSCODE_") || key.startsWith("CURSOR") || key === "TERM_PROGRAM")
|
|
119
114
|
.map(([key, value]) => `${key}=${value ?? ""}`)
|
|
120
115
|
.join("\n")
|
|
121
116
|
.toLowerCase();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!envText.includes("term_program=vscode") && !envText.includes("vscode_"))
|
|
125
|
-
|
|
117
|
+
if (envText.includes("cursor"))
|
|
118
|
+
return "cursor";
|
|
119
|
+
if (!envText.includes("term_program=vscode") && !envText.includes("vscode_"))
|
|
120
|
+
return undefined;
|
|
126
121
|
const parentCommands = await processCommandChain(process.ppid);
|
|
127
|
-
if (parentCommands.some((command) => command.toLowerCase().includes("cursor")))
|
|
122
|
+
if (parentCommands.some((command) => command.toLowerCase().includes("cursor")))
|
|
123
|
+
return "cursor";
|
|
128
124
|
return "vscode";
|
|
129
125
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const commands: string[] = [];
|
|
126
|
+
async function processCommandChain(startPid) {
|
|
127
|
+
const commands = [];
|
|
133
128
|
let pid = startPid;
|
|
134
129
|
for (let depth = 0; depth < 20 && pid > 1; depth += 1) {
|
|
135
130
|
try {
|
|
136
131
|
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "ppid=", "-o", "comm="]);
|
|
137
132
|
const line = stdout.trim();
|
|
138
|
-
if (!line)
|
|
133
|
+
if (!line)
|
|
134
|
+
break;
|
|
139
135
|
const match = line.match(/^(\d+)\s+(.+)$/);
|
|
140
|
-
if (!match)
|
|
136
|
+
if (!match)
|
|
137
|
+
break;
|
|
141
138
|
pid = Number(match[1]);
|
|
142
139
|
commands.push(match[2]);
|
|
143
|
-
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
144
142
|
break;
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
return commands;
|
|
148
146
|
}
|
|
149
|
-
|
|
150
|
-
function encodeRemotePath(remoteProjectDir: string) {
|
|
147
|
+
function encodeRemotePath(remoteProjectDir) {
|
|
151
148
|
return remoteProjectDir.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
152
149
|
}
|
|
153
|
-
|
|
154
|
-
async function openUri(uri: string) {
|
|
150
|
+
async function openUri(uri) {
|
|
155
151
|
if (process.platform === "darwin") {
|
|
156
152
|
await execFileAsync("open", [uri]);
|
|
157
|
-
}
|
|
153
|
+
}
|
|
154
|
+
else if (process.platform === "win32") {
|
|
158
155
|
await execFileAsync("cmd", ["/c", "start", "", uri]);
|
|
159
|
-
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
160
158
|
await execFileAsync("xdg-open", [uri]);
|
|
161
159
|
}
|
|
162
160
|
}
|