freestyle-sync 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/dist/src/main.js +515 -74
- package/package.json +18 -4
- package/dist/freestyle-sync.config.js +0 -35
- package/dist/main.js +0 -1319
- package/dist/plugins/agent-claude/src/index.js +0 -116
- package/dist/plugins/agent-codex/src/index.js +0 -68
- package/dist/plugins/agent-copilot/src/index.js +0 -529
- package/dist/plugins/auth-aws/src/index.js +0 -29
- package/dist/plugins/auth-azure/src/index.js +0 -29
- package/dist/plugins/auth-context.js +0 -213
- package/dist/plugins/auth-docker/src/index.js +0 -29
- package/dist/plugins/auth-env/src/index.js +0 -35
- package/dist/plugins/auth-gcloud/src/index.js +0 -29
- package/dist/plugins/auth-git/src/index.js +0 -50
- package/dist/plugins/auth-github-cli/src/index.js +0 -39
- package/dist/plugins/auth-npm/src/index.js +0 -38
- package/dist/plugins/auth-ssh/src/index.js +0 -42
- package/dist/plugins/auth-yarn/src/index.js +0 -24
- package/dist/plugins/node-npm/src/index.js +0 -388
- package/dist/plugins/npm-native-deps.js +0 -307
- package/dist/plugins/shell-history/src/index.js +0 -65
- package/dist/plugins/vscode/src/index.js +0 -160
- package/dist/src/pushvm.config.js +0 -36
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { definePlugin } from "../../../src/plugin-api.js";
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
export function nodeNpmPlugin() {
|
|
6
|
-
return definePlugin({
|
|
7
|
-
name: "@freestyle-sync/node-npm",
|
|
8
|
-
async afterProjectSync({ vm, options }) {
|
|
9
|
-
const nodeVersion = process.versions.node;
|
|
10
|
-
const npmVersion = await localNpmVersion();
|
|
11
|
-
const script = `
|
|
12
|
-
set -e
|
|
13
|
-
export HOME="\${HOME:-/root}"
|
|
14
|
-
set -u
|
|
15
|
-
cd ${shellQuote(options.remoteProjectDir)}
|
|
16
|
-
if [ ! -f package.json ]; then
|
|
17
|
-
exit 0
|
|
18
|
-
fi
|
|
19
|
-
desired_node=${shellQuote(nodeVersion)}
|
|
20
|
-
desired_npm=${shellQuote(npmVersion ?? "")}
|
|
21
|
-
nvm_version=v0.40.3
|
|
22
|
-
export NVM_DIR=/root/.nvm
|
|
23
|
-
runtime_cache=.freestyle-sync/node-runtime-cache.json
|
|
24
|
-
|
|
25
|
-
install_nvm() {
|
|
26
|
-
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
|
27
|
-
return 0
|
|
28
|
-
fi
|
|
29
|
-
mkdir -p "$NVM_DIR"
|
|
30
|
-
if command -v git >/dev/null 2>&1; then
|
|
31
|
-
rm -rf "$NVM_DIR"
|
|
32
|
-
git clone --depth 1 --branch "$nvm_version" https://github.com/nvm-sh/nvm.git "$NVM_DIR"
|
|
33
|
-
elif command -v curl >/dev/null 2>&1; then
|
|
34
|
-
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh" | PROFILE=/dev/null bash
|
|
35
|
-
elif command -v wget >/dev/null 2>&1; then
|
|
36
|
-
wget -qO- "https://raw.githubusercontent.com/nvm-sh/nvm/$nvm_version/install.sh" | PROFILE=/dev/null bash
|
|
37
|
-
else
|
|
38
|
-
echo "git, curl, or wget is required to install nvm" >&2
|
|
39
|
-
return 1
|
|
40
|
-
fi
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
install_nvm
|
|
44
|
-
. "$NVM_DIR/nvm.sh"
|
|
45
|
-
installed_node=$(nvm version "$desired_node" || true)
|
|
46
|
-
if [ "$installed_node" = "N/A" ]; then
|
|
47
|
-
echo "Installing Node $desired_node with nvm"
|
|
48
|
-
nvm install "$desired_node"
|
|
49
|
-
fi
|
|
50
|
-
nvm alias default "$desired_node" >/dev/null
|
|
51
|
-
nvm use --silent "$desired_node"
|
|
52
|
-
actual_node=$(node -p 'process.versions.node')
|
|
53
|
-
if [ "$actual_node" != "$desired_node" ]; then
|
|
54
|
-
echo "Expected Node $desired_node but activated $actual_node" >&2
|
|
55
|
-
exit 1
|
|
56
|
-
fi
|
|
57
|
-
actual_npm=$(npm --version 2>/dev/null || true)
|
|
58
|
-
if [ -n "$desired_npm" ] && [ "$actual_npm" != "$desired_npm" ]; then
|
|
59
|
-
echo "Installing npm $desired_npm for Node $desired_node"
|
|
60
|
-
npm install -g "npm@$desired_npm"
|
|
61
|
-
actual_npm=$(npm --version)
|
|
62
|
-
fi
|
|
63
|
-
mkdir -p .freestyle-sync
|
|
64
|
-
printf '{"node":"%s","npm":"%s","nvm":"%s"}\n' "$actual_node" "$actual_npm" "$nvm_version" > "$runtime_cache"
|
|
65
|
-
grep -qxF 'export NVM_DIR=/root/.nvm' /root/.profile || printf '\n%s\n' 'export NVM_DIR=/root/.nvm' >> /root/.profile
|
|
66
|
-
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
|
|
67
|
-
|
|
68
|
-
node <<'NODE'
|
|
69
|
-
const fs = require('node:fs');
|
|
70
|
-
const path = require('node:path');
|
|
71
|
-
|
|
72
|
-
const root = process.cwd();
|
|
73
|
-
const rootPackage = readJson(path.join(root, 'package.json'));
|
|
74
|
-
const workspaces = Array.isArray(rootPackage.workspaces)
|
|
75
|
-
? rootPackage.workspaces
|
|
76
|
-
: Array.isArray(rootPackage.workspaces && rootPackage.workspaces.packages)
|
|
77
|
-
? rootPackage.workspaces.packages
|
|
78
|
-
: [];
|
|
79
|
-
|
|
80
|
-
let repaired = 0;
|
|
81
|
-
for (const packageDir of unique(workspaces.flatMap(expandWorkspacePattern))) {
|
|
82
|
-
const packageJson = readJson(path.join(packageDir, 'package.json'));
|
|
83
|
-
if (typeof packageJson.name !== 'string' || packageJson.name.length === 0) continue;
|
|
84
|
-
|
|
85
|
-
const linkPath = path.join(root, 'node_modules', ...packageJson.name.split('/'));
|
|
86
|
-
const target = path.relative(path.dirname(linkPath), packageDir) || '.';
|
|
87
|
-
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const stats = fs.lstatSync(linkPath);
|
|
91
|
-
if (stats.isSymbolicLink()) {
|
|
92
|
-
const currentTarget = fs.readlinkSync(linkPath);
|
|
93
|
-
if (currentTarget === target || path.resolve(path.dirname(linkPath), currentTarget) === packageDir) continue;
|
|
94
|
-
fs.unlinkSync(linkPath);
|
|
95
|
-
} else {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (error.code !== 'ENOENT') continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
fs.symlinkSync(target, linkPath, 'dir');
|
|
103
|
-
repaired += 1;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (repaired > 0) console.log('Repaired npm workspace links: ' + repaired);
|
|
107
|
-
|
|
108
|
-
function readJson(filePath) {
|
|
109
|
-
try {
|
|
110
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
111
|
-
} catch {
|
|
112
|
-
return {};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function expandWorkspacePattern(pattern) {
|
|
117
|
-
if (typeof pattern !== 'string' || pattern.length === 0 || pattern.includes('**')) return [];
|
|
118
|
-
const segments = pattern.split(/[\\/]+/).filter(Boolean);
|
|
119
|
-
const starIndex = segments.indexOf('*');
|
|
120
|
-
if (starIndex === -1) return [path.resolve(root, ...segments)];
|
|
121
|
-
if (segments.indexOf('*', starIndex + 1) !== -1) return [];
|
|
122
|
-
|
|
123
|
-
const baseDir = path.resolve(root, ...segments.slice(0, starIndex));
|
|
124
|
-
const suffix = segments.slice(starIndex + 1);
|
|
125
|
-
let entries;
|
|
126
|
-
try {
|
|
127
|
-
entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
128
|
-
} catch {
|
|
129
|
-
return [];
|
|
130
|
-
}
|
|
131
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(baseDir, entry.name, ...suffix));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function unique(values) {
|
|
135
|
-
return Array.from(new Set(values));
|
|
136
|
-
}
|
|
137
|
-
NODE
|
|
138
|
-
|
|
139
|
-
if [ ! -d node_modules ]; then
|
|
140
|
-
exit 0
|
|
141
|
-
fi
|
|
142
|
-
package_list=$(mktemp /tmp/vmpush-native-packages.XXXXXX)
|
|
143
|
-
fingerprint_file=$(mktemp /tmp/vmpush-native-fingerprints.XXXXXX)
|
|
144
|
-
scanner_file=$(mktemp /tmp/vmpush-native-scanner.XXXXXX.js)
|
|
145
|
-
cache_file=.freestyle-sync/npm-native-deps-cache.json
|
|
146
|
-
trap 'rm -f "$package_list" "$fingerprint_file" "$scanner_file"' EXIT
|
|
147
|
-
cat > "$scanner_file" <<'NODE'
|
|
148
|
-
const fs = require('node:fs');
|
|
149
|
-
const path = require('node:path');
|
|
150
|
-
const crypto = require('node:crypto');
|
|
151
|
-
|
|
152
|
-
const cachePath = path.resolve(process.argv[2]);
|
|
153
|
-
const fingerprintPath = process.argv[3];
|
|
154
|
-
const mode = process.argv[4];
|
|
155
|
-
const nodeModules = path.resolve('node_modules');
|
|
156
|
-
const packages = new Set();
|
|
157
|
-
const packageInfos = [];
|
|
158
|
-
const incompatiblePlatformPackages = new Set();
|
|
159
|
-
const nativeFilesByPackage = new Map();
|
|
160
|
-
const platformPackagesByWrapper = new Map();
|
|
161
|
-
|
|
162
|
-
function readPackageJson(packageDir) {
|
|
163
|
-
try {
|
|
164
|
-
return JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
165
|
-
} catch {
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function addPackage(packageDir) {
|
|
171
|
-
const packageJson = readPackageJson(packageDir);
|
|
172
|
-
if (typeof packageJson?.name !== 'string' || packageJson.name.length === 0) return;
|
|
173
|
-
packageInfos.push({ dir: packageDir, json: packageJson, name: packageJson.name });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function listAllowsCurrent(value, current) {
|
|
177
|
-
if (value === undefined) return true;
|
|
178
|
-
const list = Array.isArray(value) ? value : [value];
|
|
179
|
-
const positives = list.filter((item) => typeof item === 'string' && !item.startsWith('!'));
|
|
180
|
-
const negatives = list.filter((item) => typeof item === 'string' && item.startsWith('!')).map((item) => item.slice(1));
|
|
181
|
-
if (negatives.includes(current)) return false;
|
|
182
|
-
return positives.length === 0 || positives.includes(current);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function packageAllowsCurrentPlatform(packageJson) {
|
|
186
|
-
return listAllowsCurrent(packageJson.os, process.platform) && listAllowsCurrent(packageJson.cpu, process.arch);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function dependencyNames(packageJson) {
|
|
190
|
-
return Object.keys(Object.assign({}, packageJson.dependencies, packageJson.optionalDependencies));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function addMapSet(map, key, value) {
|
|
194
|
-
const existing = map.get(key);
|
|
195
|
-
if (existing) existing.add(value);
|
|
196
|
-
else map.set(key, new Set([value]));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function relativeToProject(filePath) {
|
|
200
|
-
return path.relative(process.cwd(), filePath).split(path.sep).join('/');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function collectPackageFiles(packageDir, files) {
|
|
204
|
-
let entries;
|
|
205
|
-
try {
|
|
206
|
-
entries = fs.readdirSync(packageDir, { withFileTypes: true });
|
|
207
|
-
} catch {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
for (const entry of entries) {
|
|
212
|
-
const child = path.join(packageDir, entry.name);
|
|
213
|
-
if (entry.isDirectory()) collectPackageFiles(child, files);
|
|
214
|
-
else if (entry.isFile()) files.push(child);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function packageFingerprint(packageName) {
|
|
219
|
-
const info = packageInfos.find((candidate) => candidate.name === packageName);
|
|
220
|
-
if (!info) return undefined;
|
|
221
|
-
const files = [path.join(info.dir, 'package.json')];
|
|
222
|
-
for (const nativeFile of nativeFilesByPackage.get(packageName) || []) files.push(nativeFile);
|
|
223
|
-
for (const platformPackageName of platformPackagesByWrapper.get(packageName) || []) {
|
|
224
|
-
const platformInfo = packageInfos.find((candidate) => candidate.name === platformPackageName);
|
|
225
|
-
if (!platformInfo) continue;
|
|
226
|
-
files.push(path.join(platformInfo.dir, 'package.json'));
|
|
227
|
-
collectPackageFiles(platformInfo.dir, files);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const hash = crypto.createHash('sha256');
|
|
231
|
-
hash.update(process.platform);
|
|
232
|
-
hash.update(String.fromCharCode(0));
|
|
233
|
-
hash.update(process.arch);
|
|
234
|
-
hash.update(String.fromCharCode(0));
|
|
235
|
-
hash.update(process.versions.modules || '');
|
|
236
|
-
hash.update(String.fromCharCode(0));
|
|
237
|
-
for (const filePath of Array.from(new Set(files)).sort()) {
|
|
238
|
-
try {
|
|
239
|
-
const stats = fs.statSync(filePath);
|
|
240
|
-
if (!stats.isFile()) continue;
|
|
241
|
-
hash.update(relativeToProject(filePath));
|
|
242
|
-
hash.update(String.fromCharCode(0));
|
|
243
|
-
hash.update(String(stats.size));
|
|
244
|
-
hash.update(String.fromCharCode(0));
|
|
245
|
-
hash.update(String(stats.mtimeMs));
|
|
246
|
-
hash.update(String.fromCharCode(0));
|
|
247
|
-
hash.update(String(stats.mode));
|
|
248
|
-
hash.update(String.fromCharCode(0));
|
|
249
|
-
} catch {
|
|
250
|
-
// Ignore files that disappeared while scanning.
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return hash.digest('hex');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function nearestPackageDir(start) {
|
|
257
|
-
let current = start;
|
|
258
|
-
while (current.startsWith(nodeModules)) {
|
|
259
|
-
if (fs.existsSync(path.join(current, 'package.json'))) return current;
|
|
260
|
-
const parent = path.dirname(current);
|
|
261
|
-
if (parent === current) break;
|
|
262
|
-
current = parent;
|
|
263
|
-
}
|
|
264
|
-
return undefined;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function addNativePackage(nativeFile) {
|
|
268
|
-
const packageDir = nearestPackageDir(path.dirname(nativeFile));
|
|
269
|
-
if (!packageDir) return;
|
|
270
|
-
const packageJson = readPackageJson(packageDir);
|
|
271
|
-
if (typeof packageJson?.name !== 'string' || packageJson.name.length === 0) return;
|
|
272
|
-
packages.add(packageJson.name);
|
|
273
|
-
addMapSet(nativeFilesByPackage, packageJson.name, nativeFile);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function walk(directory) {
|
|
277
|
-
let entries;
|
|
278
|
-
try {
|
|
279
|
-
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
280
|
-
} catch {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
for (const entry of entries) {
|
|
285
|
-
const child = path.join(directory, entry.name);
|
|
286
|
-
if (entry.isDirectory()) {
|
|
287
|
-
if (entry.name !== 'node_modules' && fs.existsSync(path.join(child, 'package.json'))) addPackage(child);
|
|
288
|
-
walk(child);
|
|
289
|
-
} else if (entry.isFile() && entry.name.endsWith('.node')) {
|
|
290
|
-
addNativePackage(child);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function cacheEntryMatches(entry, fingerprint) {
|
|
296
|
-
if (entry === fingerprint) return true;
|
|
297
|
-
if (entry && typeof entry === 'object' && (entry.input === fingerprint || entry.fixed === fingerprint)) return true;
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
walk(nodeModules);
|
|
302
|
-
for (const info of packageInfos) {
|
|
303
|
-
if ((info.json.os !== undefined || info.json.cpu !== undefined) && !packageAllowsCurrentPlatform(info.json)) {
|
|
304
|
-
incompatiblePlatformPackages.add(info.name);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
for (const info of packageInfos) {
|
|
308
|
-
const platformDependencies = dependencyNames(info.json).filter((dependency) => incompatiblePlatformPackages.has(dependency));
|
|
309
|
-
if (platformDependencies.length === 0) continue;
|
|
310
|
-
packages.add(info.name);
|
|
311
|
-
for (const dependency of platformDependencies) addMapSet(platformPackagesByWrapper, info.name, dependency);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
let previous = {};
|
|
315
|
-
try {
|
|
316
|
-
previous = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
317
|
-
} catch {
|
|
318
|
-
previous = {};
|
|
319
|
-
}
|
|
320
|
-
const next = Object.assign({}, previous);
|
|
321
|
-
const changedPackages = [];
|
|
322
|
-
for (const packageName of Array.from(packages).sort()) {
|
|
323
|
-
const fingerprint = packageFingerprint(packageName);
|
|
324
|
-
if (!fingerprint) continue;
|
|
325
|
-
const previousEntry = previous[packageName];
|
|
326
|
-
if (mode !== 'record' && !cacheEntryMatches(previousEntry, fingerprint)) changedPackages.push(packageName);
|
|
327
|
-
const previousObject = previousEntry && typeof previousEntry === 'object' ? previousEntry : {};
|
|
328
|
-
next[packageName] = mode === 'record'
|
|
329
|
-
? Object.assign({}, previousObject, { fixed: fingerprint })
|
|
330
|
-
: Object.assign({}, previousObject, { input: fingerprint });
|
|
331
|
-
}
|
|
332
|
-
fs.mkdirSync(path.dirname(fingerprintPath), { recursive: true });
|
|
333
|
-
fs.writeFileSync(fingerprintPath, JSON.stringify(next, null, 2) + String.fromCharCode(10));
|
|
334
|
-
if (mode !== 'record') {
|
|
335
|
-
for (const packageName of changedPackages) console.log(packageName);
|
|
336
|
-
}
|
|
337
|
-
NODE
|
|
338
|
-
node "$scanner_file" "$cache_file" "$fingerprint_file" detect > "$package_list"
|
|
339
|
-
if [ ! -s "$package_list" ]; then
|
|
340
|
-
mkdir -p .freestyle-sync
|
|
341
|
-
cp "$fingerprint_file" "$cache_file"
|
|
342
|
-
exit 0
|
|
343
|
-
fi
|
|
344
|
-
echo "Rebuilding native Node dependencies: $(tr '\n' ' ' < "$package_list")"
|
|
345
|
-
if [ -f pnpm-lock.yaml ] && command -v corepack >/dev/null 2>&1; then
|
|
346
|
-
corepack enable >/dev/null 2>&1 || true
|
|
347
|
-
xargs pnpm rebuild < "$package_list"
|
|
348
|
-
elif [ -f yarn.lock ] && command -v corepack >/dev/null 2>&1; then
|
|
349
|
-
corepack enable >/dev/null 2>&1 || true
|
|
350
|
-
if yarn rebuild --help >/dev/null 2>&1; then
|
|
351
|
-
xargs yarn rebuild < "$package_list"
|
|
352
|
-
else
|
|
353
|
-
echo "Yarn does not support targeted rebuild here; skipping broad reinstall"
|
|
354
|
-
fi
|
|
355
|
-
elif command -v npm >/dev/null 2>&1; then
|
|
356
|
-
xargs npm rebuild < "$package_list"
|
|
357
|
-
fi
|
|
358
|
-
node "$scanner_file" "$cache_file" "$fingerprint_file" record > /dev/null
|
|
359
|
-
mkdir -p .freestyle-sync
|
|
360
|
-
cp "$fingerprint_file" "$cache_file"
|
|
361
|
-
`;
|
|
362
|
-
const scriptPath = `/tmp/vmpush-node-npm-${Date.now()}.sh`;
|
|
363
|
-
await vm.fs.writeTextFile(scriptPath, script);
|
|
364
|
-
const result = await vm.exec({
|
|
365
|
-
command: `bash ${shellQuote(scriptPath)}\nexit_code=$?\nrm -f ${shellQuote(scriptPath)}\nexit "$exit_code"`,
|
|
366
|
-
timeoutMs: 20 * 60 * 1000,
|
|
367
|
-
});
|
|
368
|
-
if (result.statusCode && result.statusCode !== 0) {
|
|
369
|
-
throw new Error(result.stderr ?? result.stdout ?? `Node/npm fixup failed with status ${result.statusCode}`);
|
|
370
|
-
}
|
|
371
|
-
if (result.stdout?.trim())
|
|
372
|
-
console.log(result.stdout.trim());
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
async function localNpmVersion() {
|
|
377
|
-
try {
|
|
378
|
-
const { stdout } = await execFileAsync("npm", ["--version"]);
|
|
379
|
-
const version = stdout.trim();
|
|
380
|
-
return version.length > 0 ? version : undefined;
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
return undefined;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
function shellQuote(value) {
|
|
387
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
388
|
-
}
|
|
@@ -1,307 +0,0 @@
|
|
|
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
|
-
}
|