@lionad/safe-npx 0.2.1 → 0.2.2
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/package.json +1 -1
- package/snpx.js +74 -50
package/package.json
CHANGED
package/snpx.js
CHANGED
|
@@ -15,35 +15,45 @@ const DEFAULT_TIME_HOURS = 24;
|
|
|
15
15
|
const DEFAULT_FALLBACK_STRATEGY = 'patch,minor,major';
|
|
16
16
|
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
17
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
|
+
|
|
18
27
|
const HELP_TEXT = `
|
|
19
|
-
safe-npx (snpx)
|
|
28
|
+
${_c.bold(_c.cyan('safe-npx (snpx)'))} — Safe npx wrapper with configurable fallback strategy
|
|
20
29
|
|
|
21
|
-
Usage:
|
|
22
|
-
snpx [options] <package>@latest [args...]
|
|
23
|
-
snpx [options] <package> [args...]
|
|
24
|
-
snpx [options] <command>
|
|
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>
|
|
25
34
|
|
|
26
|
-
Options:
|
|
27
|
-
-h, --help Show this help message
|
|
28
|
-
--time <hours> Safety window in hours (default: 24)
|
|
29
|
-
--fallback-strategy <str> Comma-separated fallback order.
|
|
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.
|
|
30
39
|
Default: patch,minor,major
|
|
31
40
|
Left-to-right: first matching safe version wins.
|
|
32
|
-
patch = version immediately before latest
|
|
33
|
-
minor = most recently published version of previous minor line
|
|
34
|
-
major = most recently published version of previous major line
|
|
35
|
-
--show-version Print resolved version and exit (no execution)
|
|
36
|
-
--self-update Check for snpx updates (safe mode, default 24h)
|
|
37
|
-
--unsafe-self-update Allow immediate snpx updates without safety window
|
|
38
|
-
|
|
39
|
-
Environment Variables:
|
|
40
|
-
SNPX_TIME Default for --time
|
|
41
|
-
SNPX_FALLBACK_STRATEGY Default for --fallback-strategy
|
|
42
|
-
|
|
43
|
-
Examples:
|
|
44
|
-
snpx -y cowsay@latest "Hello World"
|
|
45
|
-
snpx --time 48 --fallback-strategy patch,minor cowsay@latest
|
|
46
|
-
snpx --show-version cowsay@latest
|
|
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')}
|
|
47
57
|
`.trim();
|
|
48
58
|
|
|
49
59
|
/**
|
|
@@ -64,9 +74,14 @@ export function parseSemver(version) {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
/**
|
|
67
|
-
* Parse CLI arguments
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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.)
|
|
70
85
|
*/
|
|
71
86
|
export function parseArgs(argv) {
|
|
72
87
|
const args = argv.slice(2);
|
|
@@ -78,14 +93,21 @@ export function parseArgs(argv) {
|
|
|
78
93
|
time: null,
|
|
79
94
|
fallbackStrategy: null,
|
|
80
95
|
};
|
|
81
|
-
const npxArgs = [];
|
|
82
96
|
let pkgSpec = null;
|
|
83
97
|
let pkgName = null;
|
|
84
|
-
|
|
98
|
+
const restArgs = [];
|
|
99
|
+
let foundPackage = false;
|
|
85
100
|
|
|
86
101
|
for (let i = 0; i < args.length; i++) {
|
|
87
102
|
const arg = args[i];
|
|
88
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
|
|
89
111
|
if (arg === '-h' || arg === '--help') {
|
|
90
112
|
snpxFlags.help = true;
|
|
91
113
|
} else if (arg === '--show-version') {
|
|
@@ -106,30 +128,32 @@ export function parseArgs(argv) {
|
|
|
106
128
|
snpxFlags.fallbackStrategy = arg.slice('--fallback-strategy='.length);
|
|
107
129
|
} else if (arg.startsWith('--')) {
|
|
108
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);
|
|
109
134
|
} else {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
152
|
}
|
|
128
153
|
}
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
|
|
132
|
-
const restArgs = npxArgs.filter(a => a !== pkgSpec);
|
|
133
157
|
return { snpxFlags, pkgSpec, pkgName, restArgs, isLatest: !!(pkgSpec && pkgSpec.includes('@latest')) };
|
|
134
158
|
}
|
|
135
159
|
|
|
@@ -265,14 +289,14 @@ export async function checkSelfUpdate() {
|
|
|
265
289
|
try {
|
|
266
290
|
const data = await fetchPackageMetadata(PKG_NAME);
|
|
267
291
|
const latest = data['dist-tags']?.latest;
|
|
268
|
-
if (!latest) return { hasUpdate: false, currentVersion: '0.2.
|
|
292
|
+
if (!latest) return { hasUpdate: false, currentVersion: '0.2.2', latestVersion: null };
|
|
269
293
|
|
|
270
|
-
const currentVersion = '0.2.
|
|
294
|
+
const currentVersion = '0.2.2'; // Should match package.json
|
|
271
295
|
const hasUpdate = latest !== currentVersion;
|
|
272
296
|
|
|
273
297
|
return { hasUpdate, currentVersion, latestVersion: latest };
|
|
274
298
|
} catch {
|
|
275
|
-
return { hasUpdate: false, currentVersion: '0.2.
|
|
299
|
+
return { hasUpdate: false, currentVersion: '0.2.2', latestVersion: null };
|
|
276
300
|
}
|
|
277
301
|
}
|
|
278
302
|
|