@lionad/safe-npx 0.2.0 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/snpx.js +75 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionad/safe-npx",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Safe npx wrapper - lock to latest-1 version with 24h cache",
5
5
  "type": "module",
6
6
  "bin": {
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) - Safe npx wrapper with configurable fallback strategy
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
- * Separates snpx-specific flags from npx passthrough arguments.
69
- * Recognizes both @latest specifiers and bare package names.
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
- let sawFirstPositional = false;
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') {
@@ -104,30 +126,34 @@ export function parseArgs(argv) {
104
126
  snpxFlags.fallbackStrategy = args[++i];
105
127
  } else if (arg.startsWith('--fallback-strategy=')) {
106
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);
107
134
  } else {
108
- npxArgs.push(arg);
109
- if (!pkgName && !sawFirstPositional) {
110
- if (arg.startsWith('-')) {
111
- // npx flag; skip for package detection
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;
112
147
  } else {
113
- sawFirstPositional = true;
114
- const latestMatch = arg.match(/^(@[^/]+\/[^@]+|[^@]+)@latest$/);
115
- if (latestMatch) {
116
- pkgSpec = arg;
117
- pkgName = latestMatch[1];
118
- } else {
119
- const bareMatch = arg.match(/^(@[^/]+\/[^@]+|[^@]+)$/);
120
- if (bareMatch) {
121
- pkgSpec = arg;
122
- pkgName = bareMatch[1];
123
- }
124
- }
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);
125
152
  }
126
153
  }
127
154
  }
128
155
  }
129
156
 
130
- const restArgs = npxArgs.filter(a => a !== pkgSpec);
131
157
  return { snpxFlags, pkgSpec, pkgName, restArgs, isLatest: !!(pkgSpec && pkgSpec.includes('@latest')) };
132
158
  }
133
159
 
@@ -263,14 +289,14 @@ export async function checkSelfUpdate() {
263
289
  try {
264
290
  const data = await fetchPackageMetadata(PKG_NAME);
265
291
  const latest = data['dist-tags']?.latest;
266
- if (!latest) return { hasUpdate: false, currentVersion: '0.2.0', latestVersion: null };
292
+ if (!latest) return { hasUpdate: false, currentVersion: '0.2.2', latestVersion: null };
267
293
 
268
- const currentVersion = '0.2.0'; // Should match package.json
294
+ const currentVersion = '0.2.2'; // Should match package.json
269
295
  const hasUpdate = latest !== currentVersion;
270
296
 
271
297
  return { hasUpdate, currentVersion, latestVersion: latest };
272
298
  } catch {
273
- return { hasUpdate: false, currentVersion: '0.2.0', latestVersion: null };
299
+ return { hasUpdate: false, currentVersion: '0.2.2', latestVersion: null };
274
300
  }
275
301
  }
276
302