@lionad/safe-npx 0.2.3 → 0.3.0
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 +73 -6
- package/dist/index.js +1899 -0
- package/package.json +14 -4
- package/snpx.js +0 -427
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lionad/safe-npx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Safe npx wrapper - lock to latest-1 version with 24h cache",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"snpx": "
|
|
7
|
+
"snpx": "dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"main": "dist/index.js",
|
|
9
10
|
"files": [
|
|
10
|
-
"
|
|
11
|
+
"dist/"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
|
-
"
|
|
14
|
+
"build": "vite build",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
14
17
|
},
|
|
15
18
|
"keywords": [
|
|
16
19
|
"npx",
|
|
@@ -25,6 +28,13 @@
|
|
|
25
28
|
"node": ">=18.0.0"
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
31
|
+
"@types/node": "^25.5.0",
|
|
32
|
+
"@types/semver": "^7.7.1",
|
|
33
|
+
"typescript": "^5.4.0",
|
|
34
|
+
"vite": "^8.0.3",
|
|
28
35
|
"vitest": "^1.6.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"semver": "^7.7.4"
|
|
29
39
|
}
|
|
30
40
|
}
|
package/snpx.js
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* safe-npx (snpx) - Safe npx wrapper with configurable fallback strategy
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { spawn } from 'child_process';
|
|
7
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
|
|
11
|
-
const CACHE_DIR = join(homedir(), '.cache', 'snpx');
|
|
12
|
-
const REGISTRY = 'https://registry.npmjs.org';
|
|
13
|
-
const PKG_NAME = '@lionad/safe-npx';
|
|
14
|
-
const DEFAULT_TIME_HOURS = 24;
|
|
15
|
-
const DEFAULT_FALLBACK_STRATEGY = 'patch,minor,major';
|
|
16
|
-
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
17
|
-
|
|
18
|
-
const _tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
19
|
-
const _c = {
|
|
20
|
-
bold: (s) => _tty ? `\x1b[1m${s}\x1b[22m` : s,
|
|
21
|
-
dim: (s) => _tty ? `\x1b[2m${s}\x1b[22m` : s,
|
|
22
|
-
green: (s) => _tty ? `\x1b[32m${s}\x1b[39m` : s,
|
|
23
|
-
cyan: (s) => _tty ? `\x1b[36m${s}\x1b[39m` : s,
|
|
24
|
-
yellow: (s) => _tty ? `\x1b[33m${s}\x1b[39m` : s,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const HELP_TEXT = `
|
|
28
|
-
${_c.bold(_c.cyan('safe-npx (snpx)'))} — Safe npx wrapper with configurable fallback strategy
|
|
29
|
-
|
|
30
|
-
${_c.bold('Usage:')}
|
|
31
|
-
${_c.green('snpx')} [options] <package>@latest [args...]
|
|
32
|
-
${_c.green('snpx')} [options] <package> [args...]
|
|
33
|
-
${_c.green('snpx')} [options] <command>
|
|
34
|
-
|
|
35
|
-
${_c.bold('Options:')}
|
|
36
|
-
${_c.yellow('-h, --help')} Show this help message
|
|
37
|
-
${_c.yellow('--time')} ${_c.dim('<hours>')} Safety window in hours (default: 24)
|
|
38
|
-
${_c.yellow('--fallback-strategy')} ${_c.dim('<str>')} Comma-separated fallback order.
|
|
39
|
-
Default: patch,minor,major
|
|
40
|
-
Left-to-right: first matching safe version wins.
|
|
41
|
-
${_c.dim('patch')} = version immediately before latest
|
|
42
|
-
${_c.dim('minor')} = most recently published version of previous minor line
|
|
43
|
-
${_c.dim('major')} = most recently published version of previous major line
|
|
44
|
-
${_c.yellow('--show-version')} Print resolved version and exit (no execution)
|
|
45
|
-
${_c.yellow('--self-update')} Check for snpx updates (safe mode, default 24h)
|
|
46
|
-
${_c.yellow('--unsafe-self-update')} Allow immediate snpx updates without safety window
|
|
47
|
-
|
|
48
|
-
${_c.bold('Environment Variables:')}
|
|
49
|
-
${_c.green('SNPX_TIME')} Default for --time
|
|
50
|
-
${_c.green('SNPX_FALLBACK_STRATEGY')} Default for --fallback-strategy
|
|
51
|
-
|
|
52
|
-
${_c.bold('Examples:')}
|
|
53
|
-
${_c.green('snpx')} -y cowsay@latest "Hello World"
|
|
54
|
-
${_c.green('snpx')} --time 48 --fallback-strategy patch,minor cowsay@latest
|
|
55
|
-
${_c.green('snpx')} --show-version cowsay@latest
|
|
56
|
-
${_c.green('snpx')} cowsay@latest --version ${_c.dim('# passes --version to cowsay')}
|
|
57
|
-
`.trim();
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Parse a simple semver string into components.
|
|
61
|
-
* Returns null for invalid or complex prerelease strings.
|
|
62
|
-
*/
|
|
63
|
-
export function parseSemver(version) {
|
|
64
|
-
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/);
|
|
65
|
-
if (!match) return null;
|
|
66
|
-
return {
|
|
67
|
-
major: parseInt(match[1], 10),
|
|
68
|
-
minor: parseInt(match[2], 10),
|
|
69
|
-
patch: parseInt(match[3], 10),
|
|
70
|
-
prerelease: match[4] || null,
|
|
71
|
-
build: match[5] || null,
|
|
72
|
-
raw: version
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Parse CLI arguments using two-phase parsing:
|
|
78
|
-
*
|
|
79
|
-
* Phase 1 (before package): Only snpx flags accepted.
|
|
80
|
-
* Unknown --flags → error. Single-dash flags (-y) → npx passthrough.
|
|
81
|
-
* Phase 2 (after package): Everything is passthrough to the executed tool.
|
|
82
|
-
*
|
|
83
|
-
* snpx [snpx-flags] <package> [tool-args...]
|
|
84
|
-
* snpx [snpx-flags] (--help, --self-update, etc.)
|
|
85
|
-
*/
|
|
86
|
-
export function parseArgs(argv) {
|
|
87
|
-
const args = argv.slice(2);
|
|
88
|
-
const snpxFlags = {
|
|
89
|
-
help: false,
|
|
90
|
-
showVersion: false,
|
|
91
|
-
selfUpdate: false,
|
|
92
|
-
unsafeSelfUpdate: false,
|
|
93
|
-
time: null,
|
|
94
|
-
fallbackStrategy: null,
|
|
95
|
-
};
|
|
96
|
-
let pkgSpec = null;
|
|
97
|
-
let pkgName = null;
|
|
98
|
-
const restArgs = [];
|
|
99
|
-
let foundPackage = false;
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < args.length; i++) {
|
|
102
|
-
const arg = args[i];
|
|
103
|
-
|
|
104
|
-
// Phase 2: after package name, everything is passthrough
|
|
105
|
-
if (foundPackage) {
|
|
106
|
-
restArgs.push(arg);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Phase 1: before package, only known snpx flags accepted
|
|
111
|
-
if (arg === '-h' || arg === '--help') {
|
|
112
|
-
snpxFlags.help = true;
|
|
113
|
-
} else if (arg === '--show-version') {
|
|
114
|
-
snpxFlags.showVersion = true;
|
|
115
|
-
} else if (arg === '--self-update') {
|
|
116
|
-
snpxFlags.selfUpdate = true;
|
|
117
|
-
} else if (arg === '--unsafe-self-update') {
|
|
118
|
-
snpxFlags.unsafeSelfUpdate = true;
|
|
119
|
-
} else if (arg === '--time') {
|
|
120
|
-
if (i + 1 >= args.length) throw new Error('Missing value for --time');
|
|
121
|
-
snpxFlags.time = args[++i];
|
|
122
|
-
} else if (arg.startsWith('--time=')) {
|
|
123
|
-
snpxFlags.time = arg.slice('--time='.length);
|
|
124
|
-
} else if (arg === '--fallback-strategy') {
|
|
125
|
-
if (i + 1 >= args.length) throw new Error('Missing value for --fallback-strategy');
|
|
126
|
-
snpxFlags.fallbackStrategy = args[++i];
|
|
127
|
-
} else if (arg.startsWith('--fallback-strategy=')) {
|
|
128
|
-
snpxFlags.fallbackStrategy = arg.slice('--fallback-strategy='.length);
|
|
129
|
-
} else if (arg.startsWith('--')) {
|
|
130
|
-
throw new Error(`Unknown flag: ${arg}. Run 'snpx --help' for available options.`);
|
|
131
|
-
} else if (arg.startsWith('-')) {
|
|
132
|
-
// Single-dash npx flags (e.g. -y, -p) are always passthrough
|
|
133
|
-
restArgs.push(arg);
|
|
134
|
-
} else {
|
|
135
|
-
// Positional: check if it's a package name
|
|
136
|
-
const latestMatch = arg.match(/^(@[^/]+\/[^@]+|[^@]+)@latest$/);
|
|
137
|
-
if (latestMatch) {
|
|
138
|
-
pkgSpec = arg;
|
|
139
|
-
pkgName = latestMatch[1];
|
|
140
|
-
foundPackage = true;
|
|
141
|
-
} else {
|
|
142
|
-
const bareMatch = arg.match(/^(@[^/]+\/[^@]+|[^@]+)$/);
|
|
143
|
-
if (bareMatch) {
|
|
144
|
-
pkgSpec = arg;
|
|
145
|
-
pkgName = bareMatch[1];
|
|
146
|
-
foundPackage = true;
|
|
147
|
-
} else {
|
|
148
|
-
// Not a recognized package spec (e.g. pkg@1.0.0).
|
|
149
|
-
// Stop phase 1; treat this and everything after as npx passthrough.
|
|
150
|
-
foundPackage = true;
|
|
151
|
-
restArgs.push(arg);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return { snpxFlags, pkgSpec, pkgName, restArgs, isLatest: !!(pkgSpec && pkgSpec.includes('@latest')) };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Read cached version if valid.
|
|
162
|
-
* Cache TTL now follows the safety window (timeMs).
|
|
163
|
-
*/
|
|
164
|
-
export function getCachedVersion(pkgName, ttlMs = DEFAULT_TIME_HOURS * MS_PER_HOUR) {
|
|
165
|
-
const cacheFile = join(CACHE_DIR, `${pkgName.replaceAll('/', '--')}.json`);
|
|
166
|
-
if (!existsSync(cacheFile)) return null;
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const data = JSON.parse(readFileSync(cacheFile, 'utf8'));
|
|
170
|
-
const age = Date.now() - (data.resolvedAt || 0);
|
|
171
|
-
if (age > ttlMs) return null;
|
|
172
|
-
return data.version;
|
|
173
|
-
} catch {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Write version to cache.
|
|
180
|
-
*/
|
|
181
|
-
export function setCachedVersion(pkgName, version) {
|
|
182
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
183
|
-
const cacheFile = join(CACHE_DIR, `${pkgName.replaceAll('/', '--')}.json`);
|
|
184
|
-
writeFileSync(cacheFile, JSON.stringify({ version, resolvedAt: Date.now() }));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Fetch package metadata from registry.
|
|
189
|
-
*/
|
|
190
|
-
export async function fetchPackageMetadata(pkgName) {
|
|
191
|
-
const url = `${REGISTRY}/${encodeURIComponent(pkgName)}`;
|
|
192
|
-
const res = await fetch(url, {
|
|
193
|
-
headers: { accept: 'application/json' },
|
|
194
|
-
signal: AbortSignal.timeout(10000),
|
|
195
|
-
});
|
|
196
|
-
if (!res.ok) throw new Error(`Failed to fetch ${pkgName}: ${res.status}`);
|
|
197
|
-
return res.json();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function buildVersionList(data) {
|
|
201
|
-
return Object.entries(data.time || {})
|
|
202
|
-
.filter(([v]) => v !== 'created' && v !== 'modified')
|
|
203
|
-
.map(([v, t]) => ({ version: v, time: new Date(t).getTime() }))
|
|
204
|
-
.filter(v => !Number.isNaN(v.time))
|
|
205
|
-
.sort((a, b) => b.time - a.time);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function findPatchFallback(versions, latestVersion) {
|
|
209
|
-
const idx = versions.findIndex(v => v.version === latestVersion);
|
|
210
|
-
if (idx === -1 || idx + 1 >= versions.length) return null;
|
|
211
|
-
return versions[idx + 1];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function findMinorFallback(versions, latestParsed) {
|
|
215
|
-
for (const v of versions) {
|
|
216
|
-
const p = parseSemver(v.version);
|
|
217
|
-
if (p && p.major === latestParsed.major && p.minor < latestParsed.minor) {
|
|
218
|
-
return v;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function findMajorFallback(versions, latestParsed) {
|
|
225
|
-
for (const v of versions) {
|
|
226
|
-
const p = parseSemver(v.version);
|
|
227
|
-
if (p && p.major < latestParsed.major) {
|
|
228
|
-
return v;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Resolve a safe version for the package.
|
|
236
|
-
*
|
|
237
|
-
* @param {string} pkgName
|
|
238
|
-
* @param {object} options
|
|
239
|
-
* @param {number} options.timeMs - Safety window in milliseconds (default: 24h)
|
|
240
|
-
* @param {string[]} options.strategy - Fallback order, e.g. ['patch','minor','major']
|
|
241
|
-
*/
|
|
242
|
-
export async function resolveSafeVersion(pkgName, options = {}) {
|
|
243
|
-
const timeMs = options.timeMs ?? DEFAULT_TIME_HOURS * MS_PER_HOUR;
|
|
244
|
-
const strategy = options.strategy ?? DEFAULT_FALLBACK_STRATEGY.split(',').map(s => s.trim());
|
|
245
|
-
|
|
246
|
-
const data = await fetchPackageMetadata(pkgName);
|
|
247
|
-
const latestVersion = data['dist-tags']?.latest;
|
|
248
|
-
if (!latestVersion) throw new Error('No latest tag found');
|
|
249
|
-
|
|
250
|
-
const latestTimeStr = data.time?.[latestVersion];
|
|
251
|
-
const latestTime = latestTimeStr ? new Date(latestTimeStr).getTime() : null;
|
|
252
|
-
|
|
253
|
-
if (!latestTime) {
|
|
254
|
-
throw new Error(`Registry did not provide a publish time for ${pkgName}@${latestVersion}. Cannot verify safety window.`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// If latest itself is old enough, use it directly.
|
|
258
|
-
if ((Date.now() - latestTime) >= timeMs) {
|
|
259
|
-
return latestVersion;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const versions = buildVersionList(data);
|
|
263
|
-
const latestParsed = parseSemver(latestVersion);
|
|
264
|
-
if (!latestParsed) throw new Error(`Unable to parse latest version ${latestVersion}`);
|
|
265
|
-
|
|
266
|
-
for (const strat of strategy) {
|
|
267
|
-
let candidate = null;
|
|
268
|
-
if (strat === 'patch') {
|
|
269
|
-
candidate = findPatchFallback(versions, latestVersion);
|
|
270
|
-
} else if (strat === 'minor') {
|
|
271
|
-
candidate = findMinorFallback(versions, latestParsed);
|
|
272
|
-
} else if (strat === 'major') {
|
|
273
|
-
candidate = findMajorFallback(versions, latestParsed);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (candidate && (Date.now() - candidate.time) >= timeMs) {
|
|
277
|
-
return candidate.version;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
throw new Error(`Could not find a safe version for ${pkgName} within strategy [${strategy.join(',')}] and time window ${Math.floor(timeMs / MS_PER_HOUR)}h`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Check if snpx itself has an update available.
|
|
286
|
-
* Returns { hasUpdate: boolean, currentVersion: string, latestVersion: string|null }
|
|
287
|
-
*/
|
|
288
|
-
export async function checkSelfUpdate() {
|
|
289
|
-
try {
|
|
290
|
-
const data = await fetchPackageMetadata(PKG_NAME);
|
|
291
|
-
const latest = data['dist-tags']?.latest;
|
|
292
|
-
if (!latest) return { hasUpdate: false, currentVersion: '0.2.3', latestVersion: null };
|
|
293
|
-
|
|
294
|
-
const currentVersion = '0.2.3'; // Should match package.json
|
|
295
|
-
const hasUpdate = latest !== currentVersion;
|
|
296
|
-
|
|
297
|
-
return { hasUpdate, currentVersion, latestVersion: latest };
|
|
298
|
-
} catch {
|
|
299
|
-
return { hasUpdate: false, currentVersion: '0.2.3', latestVersion: null };
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Run npx with proper exit code handling.
|
|
305
|
-
*/
|
|
306
|
-
function runNpx(args) {
|
|
307
|
-
return new Promise((resolve, reject) => {
|
|
308
|
-
const child = spawn('npx', args, { stdio: 'inherit' });
|
|
309
|
-
child.on('close', (code) => {
|
|
310
|
-
process.exitCode = code ?? 0;
|
|
311
|
-
resolve();
|
|
312
|
-
});
|
|
313
|
-
child.on('error', reject);
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Build effective execution options from CLI flags and environment variables.
|
|
319
|
-
*/
|
|
320
|
-
export function buildOptions(snpxFlags) {
|
|
321
|
-
const envTime = process.env.SNPX_TIME;
|
|
322
|
-
const envStrategy = process.env.SNPX_FALLBACK_STRATEGY;
|
|
323
|
-
const timeHours = snpxFlags.time ?? envTime ?? DEFAULT_TIME_HOURS;
|
|
324
|
-
const parsedHours = parseFloat(timeHours);
|
|
325
|
-
if (!Number.isFinite(parsedHours) || parsedHours < 0) {
|
|
326
|
-
console.error(`[snpx] Invalid --time value: ${timeHours}`);
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
const timeMs = parsedHours * MS_PER_HOUR;
|
|
330
|
-
const strategyStr = snpxFlags.fallbackStrategy ?? envStrategy ?? DEFAULT_FALLBACK_STRATEGY;
|
|
331
|
-
const strategy = strategyStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
332
|
-
return { timeHours, timeMs, strategy };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Main entry
|
|
337
|
-
*/
|
|
338
|
-
async function main() {
|
|
339
|
-
const { snpxFlags, pkgSpec, pkgName, restArgs } = parseArgs(process.argv);
|
|
340
|
-
|
|
341
|
-
if (snpxFlags.help) {
|
|
342
|
-
console.log(HELP_TEXT);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const { timeHours, timeMs, strategy } = buildOptions(snpxFlags);
|
|
347
|
-
|
|
348
|
-
// Handle self-update check.
|
|
349
|
-
const selfUpdate = snpxFlags.selfUpdate;
|
|
350
|
-
const unsafeSelfUpdate = snpxFlags.unsafeSelfUpdate;
|
|
351
|
-
|
|
352
|
-
if (selfUpdate || unsafeSelfUpdate) {
|
|
353
|
-
const unsafe = unsafeSelfUpdate;
|
|
354
|
-
console.error(`[snpx] Checking for updates${unsafe ? ' (unsafe mode)' : ''}...`);
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
const { hasUpdate, currentVersion, latestVersion } = await checkSelfUpdate();
|
|
358
|
-
|
|
359
|
-
if (!latestVersion) {
|
|
360
|
-
console.error('[snpx] Could not check for updates. Try again later.');
|
|
361
|
-
process.exit(1);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (hasUpdate) {
|
|
365
|
-
console.error(`[snpx] Update available: ${currentVersion} → ${latestVersion}`);
|
|
366
|
-
console.error('[snpx] Run: npm update -g @lionad/safe-npx');
|
|
367
|
-
|
|
368
|
-
if (!unsafe) {
|
|
369
|
-
const data = await fetchPackageMetadata(PKG_NAME);
|
|
370
|
-
const times = data.time || {};
|
|
371
|
-
const latestTime = times[latestVersion];
|
|
372
|
-
if (latestTime) {
|
|
373
|
-
const age = Date.now() - new Date(latestTime).getTime();
|
|
374
|
-
if (age < timeMs) {
|
|
375
|
-
console.error(`[snpx] Warning: Latest version is only ${Math.floor(age / MS_PER_HOUR)}h old. Waiting for ${timeHours}h safety window.`);
|
|
376
|
-
console.error('[snpx] Use --unsafe-self-update to bypass (not recommended)');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
} else {
|
|
381
|
-
console.error(`[snpx] Already up to date (${currentVersion})`);
|
|
382
|
-
}
|
|
383
|
-
} catch (err) {
|
|
384
|
-
console.error(`[snpx] Error checking for updates: ${err.message}`);
|
|
385
|
-
process.exit(1);
|
|
386
|
-
}
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Normal package execution flow.
|
|
391
|
-
// No package specifier found -> pass through to npx (but strip snpx flags).
|
|
392
|
-
if (!pkgSpec || !pkgName) {
|
|
393
|
-
await runNpx(restArgs);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Check cache first.
|
|
398
|
-
let version = getCachedVersion(pkgName, timeMs);
|
|
399
|
-
|
|
400
|
-
if (!version) {
|
|
401
|
-
console.error(`[snpx] Resolving safe version for ${pkgName}...`);
|
|
402
|
-
try {
|
|
403
|
-
version = await resolveSafeVersion(pkgName, { timeMs, strategy });
|
|
404
|
-
setCachedVersion(pkgName, version);
|
|
405
|
-
console.error(`[snpx] Using ${pkgName}@${version} (strategy: ${strategy.join(',')}, window: ${timeHours}h)`);
|
|
406
|
-
} catch (err) {
|
|
407
|
-
console.error(`[snpx] Error: ${err.message}`);
|
|
408
|
-
process.exit(1);
|
|
409
|
-
}
|
|
410
|
-
} else {
|
|
411
|
-
console.error(`[snpx] Using cached ${pkgName}@${version}`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (snpxFlags.showVersion) {
|
|
415
|
-
console.log(version);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Replace package specifier with pinned version and spawn npx.
|
|
420
|
-
const npxArgs = [`${pkgName}@${version}`, ...restArgs];
|
|
421
|
-
await runNpx(npxArgs);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
main().catch(err => {
|
|
425
|
-
console.error(`[snpx] Fatal: ${err.message}`);
|
|
426
|
-
process.exit(1);
|
|
427
|
-
});
|