postman-cli-completion 1.71.0 → 1.74.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.
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // Postinstall version-drift notice.
3
+ //
4
+ // Runs once, right after `npm i -g postman-cli-completion`, and tells the user
5
+ // if their installed Postman CLI does not match the version these completions
6
+ // were generated for. That moment is the useful one: the user is already doing
7
+ // package management, so an "upgrade this" message is actionable.
8
+ //
9
+ // Hard rules for this file, because postinstall scripts are bad neighbours:
10
+ //
11
+ // 1. NEVER fail the install. Every path exits 0, including unexpected throws.
12
+ // 2. NEVER touch the network. A registry lookup here is exactly the kind of
13
+ // postinstall behaviour people (rightly) disable; the offline comparison
14
+ // already catches the common case. `doctor` does the online check on
15
+ // demand.
16
+ // 3. Say nothing when there is nothing to say, and stay quiet in CI and other
17
+ // non-interactive installs.
18
+ //
19
+ // The online "is my CLI older than npm latest?" check lives in `doctor`.
20
+
21
+ import { resolve, dirname } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { checkVersions } from "./version-check.mjs";
24
+
25
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
26
+
27
+ /**
28
+ * Should we stay silent regardless of what the versions say?
29
+ *
30
+ * Suppressed for:
31
+ * - an explicit opt-out
32
+ * - CI (the notice is for humans; it would just be log noise)
33
+ * - a non-TTY stderr, which also covers piped/scripted installs
34
+ * - local (non-global) installs: this package is a global CLI helper, so a
35
+ * local install is usually a dependency graph artifact, not a user choice
36
+ */
37
+ export function shouldSuppress(env = process.env, isTTY = process.stderr.isTTY) {
38
+ if (env.POSTMAN_COMPLETION_NO_VERSION_CHECK === "1") return true;
39
+ if (env.CI && env.CI !== "false") return true;
40
+ if (env.npm_config_global !== "true") return true;
41
+ return !isTTY;
42
+ }
43
+
44
+ /** Wrap the drift lines in a visually distinct block so it is not lost in npm's output. */
45
+ export function formatNotice(lines) {
46
+ const body = lines.map((l) => ` ${l}`).join("\n");
47
+ return `\npostman-cli-completion:\n${body}\n\n Silence this check with POSTMAN_COMPLETION_NO_VERSION_CHECK=1\n`;
48
+ }
49
+
50
+ async function main() {
51
+ if (shouldSuppress()) return;
52
+
53
+ const report = await checkVersions({
54
+ specPath: resolve(PKG_ROOT, "spec/commands.json"),
55
+ packageJsonPath: resolve(PKG_ROOT, "package.json"),
56
+ offline: true, // rule 2: no network during install
57
+ timeoutMs: 3000, // keep the install snappy even if the CLI hangs
58
+ });
59
+
60
+ // Only speak up about real, actionable drift. A matching version, a missing
61
+ // CLI, or an undeterminable one are all silent: a fresh `npm i -g` of this
62
+ // package should not lecture someone who has not installed the CLI yet.
63
+ if (!report.drifted) return;
64
+
65
+ process.stderr.write(formatNotice(report.lines));
66
+ }
67
+
68
+ // Only run when invoked as the postinstall script, not when imported by tests.
69
+ // (Matches the isMainModule guard in scripts/generate.mjs.)
70
+ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
71
+ if (isMainModule) {
72
+ // Rule 1: an install must never fail because of this notice.
73
+ try {
74
+ await main();
75
+ } catch {
76
+ // Intentionally silent — a broken notice is not worth a failed install.
77
+ }
78
+ process.exit(0);
79
+ }
@@ -0,0 +1,261 @@
1
+ // Version drift detection for the Postman CLI.
2
+ //
3
+ // Two independent comparisons, because "outdated" has two meanings here:
4
+ //
5
+ // 1. installed CLI vs. the version these completions were generated against
6
+ // (spec/commands.json.postmanCliVersion). A mismatch means completions may
7
+ // offer commands the user's CLI lacks, or miss ones it has. Offline, free.
8
+ // 2. installed CLI vs. the latest published postman-cli on npm. A mismatch
9
+ // means the user should upgrade. Requires the network.
10
+ //
11
+ // Everything that decides *what* to say is a pure function so it can be tested
12
+ // without a CLI, a network, or a filesystem.
13
+
14
+ import { execFile } from "node:child_process";
15
+ import { readFileSync } from "node:fs";
16
+
17
+ export const REGISTRY_URL = "https://registry.npmjs.org/postman-cli/latest";
18
+ export const UPGRADE_COMMAND = "npm install -g postman-cli@latest";
19
+
20
+ /**
21
+ * Parse a semver-ish string into comparable parts.
22
+ * Tolerates a leading "v", trailing prerelease/build metadata, and surrounding
23
+ * whitespace. Returns null for anything we cannot order numerically.
24
+ */
25
+ export function parseVersion(raw) {
26
+ if (typeof raw !== "string") return null;
27
+ const m = /^\s*v?(\d+)\.(\d+)\.(\d+)(?:[-+]([0-9A-Za-z.-]+))?\s*$/.exec(raw);
28
+ if (!m) return null;
29
+ return {
30
+ major: Number(m[1]),
31
+ minor: Number(m[2]),
32
+ patch: Number(m[3]),
33
+ prerelease: m[4] ?? null,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Compare two version strings. Returns -1 / 0 / 1 (a<b / a==b / a>b), or null
39
+ * if either side is unparseable — callers must treat null as "unknown", never
40
+ * as "equal", so we don't warn on garbage input.
41
+ *
42
+ * A prerelease sorts below the same x.y.z release (1.2.3-beta < 1.2.3), which
43
+ * matches semver. We deliberately do not order two different prereleases of the
44
+ * same release against each other beyond a plain string compare.
45
+ */
46
+ export function compareVersions(a, b) {
47
+ const pa = parseVersion(a);
48
+ const pb = parseVersion(b);
49
+ if (!pa || !pb) return null;
50
+
51
+ for (const part of ["major", "minor", "patch"]) {
52
+ if (pa[part] !== pb[part]) return pa[part] < pb[part] ? -1 : 1;
53
+ }
54
+ if (pa.prerelease === pb.prerelease) return 0;
55
+ if (pa.prerelease === null) return 1; // release > prerelease
56
+ if (pb.prerelease === null) return -1;
57
+ return pa.prerelease < pb.prerelease ? -1 : 1;
58
+ }
59
+
60
+ /**
61
+ * Read the installed Postman CLI version by shelling out to `postman --version`,
62
+ * which prints a bare "1.73.0".
63
+ *
64
+ * Never throws: a missing CLI, a non-zero exit, or a hang are all normal states
65
+ * for this tool (the completions package is installed independently of the CLI).
66
+ * Resolves { version, error } where exactly one side is set.
67
+ */
68
+ export function detectInstalledVersion({ command = "postman", timeoutMs = 5000 } = {}) {
69
+ return new Promise((resolvePromise) => {
70
+ execFile(command, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
71
+ if (err) {
72
+ const reason =
73
+ err.code === "ENOENT"
74
+ ? `"${command}" not found on PATH`
75
+ : err.killed
76
+ ? `"${command} --version" timed out`
77
+ : `"${command} --version" failed: ${err.message.trim()}`;
78
+ resolvePromise({ version: null, error: reason });
79
+ return;
80
+ }
81
+ // Some builds print the version among other noise; take the first
82
+ // semver-looking token so we degrade gracefully rather than fail.
83
+ const match = /v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/.exec(stdout ?? "");
84
+ if (!match) {
85
+ resolvePromise({
86
+ version: null,
87
+ error: `could not parse a version from "${command} --version" output`,
88
+ });
89
+ return;
90
+ }
91
+ resolvePromise({ version: match[0].replace(/^v/, ""), error: null });
92
+ });
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Version of the Postman CLI that the committed spec (and therefore the shipped
98
+ * completion scripts) was generated against.
99
+ */
100
+ export function readSpecVersion(specPath) {
101
+ try {
102
+ const v = JSON.parse(readFileSync(specPath, "utf8")).postmanCliVersion;
103
+ return typeof v === "string" ? v : null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * The CLI version these completions target.
111
+ *
112
+ * Two sources, in order:
113
+ * 1. spec/commands.json — present in a git checkout, the real source of truth.
114
+ * 2. this package's own version — release.yml runs `npm version <spec
115
+ * version>` before publishing, so an installed copy's version *is* the
116
+ * targeted Postman CLI version. This keeps the 348 kB spec out of the
117
+ * published tarball.
118
+ * In a git checkout package.json holds the placeholder
119
+ * "0.0.0-managed-by-spec". That is technically valid semver (a prerelease of
120
+ * 0.0.0), so it must be rejected explicitly — otherwise every real CLI would
121
+ * compare as "newer" and trigger a bogus drift warning.
122
+ */
123
+ export function resolveTargetVersion({ specPath, packageJsonPath }) {
124
+ const fromSpec = readSpecVersion(specPath);
125
+ if (fromSpec) return fromSpec;
126
+
127
+ try {
128
+ const v = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
129
+ const parsed = parseVersion(v);
130
+ if (!parsed) return null;
131
+ // Never trust a 0.0.0 placeholder as a real target version.
132
+ if (parsed.major === 0 && parsed.minor === 0 && parsed.patch === 0) return null;
133
+ return v;
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Ask npm for the latest published postman-cli version.
141
+ * Never throws — offline is an expected state, so failure is reported as data.
142
+ */
143
+ export async function fetchLatestVersion({
144
+ url = REGISTRY_URL,
145
+ timeoutMs = 5000,
146
+ fetchImpl = globalThis.fetch,
147
+ } = {}) {
148
+ const signal = AbortSignal.timeout(timeoutMs);
149
+ try {
150
+ const res = await fetchImpl(url, {
151
+ headers: { Accept: "application/json" },
152
+ signal,
153
+ });
154
+ if (!res.ok) return { version: null, error: `registry request failed: ${res.status}` };
155
+ const body = await res.json();
156
+ const version = typeof body?.version === "string" ? body.version : null;
157
+ return version
158
+ ? { version, error: null }
159
+ : { version: null, error: "registry response had no version field" };
160
+ } catch (err) {
161
+ const offline = err?.name === "TimeoutError" || err?.name === "AbortError";
162
+ return {
163
+ version: null,
164
+ error: offline ? "registry request timed out" : `registry request failed: ${err.message}`,
165
+ };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Decide what to report, given the three versions. Pure.
171
+ *
172
+ * `installed` / `spec` / `latest` may each be null (unknown). Returns a report:
173
+ * { level, lines, outdated, drifted }
174
+ * where level is "ok" | "notice" | "warn" and lines are ready to print.
175
+ */
176
+ export function buildReport({ installed, spec, latest }) {
177
+ const lines = [];
178
+ let level = "ok";
179
+ const bump = (next) => {
180
+ const rank = { ok: 0, notice: 1, warn: 2 };
181
+ if (rank[next] > rank[level]) level = next;
182
+ };
183
+
184
+ if (!installed) {
185
+ return {
186
+ level: "notice",
187
+ outdated: false,
188
+ drifted: false,
189
+ lines: [
190
+ "Postman CLI not detected — install it with: npm install -g postman-cli",
191
+ "(this package only provides shell completions; it does not include the CLI)",
192
+ ],
193
+ };
194
+ }
195
+
196
+ // 1. installed vs. latest published — the upgrade suggestion.
197
+ const vsLatest = compareVersions(installed, latest);
198
+ const outdated = vsLatest === -1;
199
+ if (outdated) {
200
+ bump("warn");
201
+ lines.push(
202
+ `Your Postman CLI is outdated: installed ${installed}, latest ${latest}.`,
203
+ ` Upgrade with: ${UPGRADE_COMMAND}`,
204
+ );
205
+ } else if (vsLatest === 1) {
206
+ lines.push(`Installed Postman CLI ${installed} is newer than npm latest ${latest}.`);
207
+ } else if (vsLatest === 0) {
208
+ lines.push(`Postman CLI ${installed} is up to date.`);
209
+ }
210
+
211
+ // 2. installed vs. the spec these completions were built from — the drift
212
+ // warning. Reported separately: being on the latest CLI does not guarantee
213
+ // the completions were regenerated for it yet, and vice versa.
214
+ const vsSpec = compareVersions(installed, spec);
215
+ const drifted = vsSpec !== null && vsSpec !== 0;
216
+ if (drifted) {
217
+ bump("warn");
218
+ lines.push(
219
+ `These completions were generated for Postman CLI ${spec}, but ${installed} is installed.`,
220
+ vsSpec === -1
221
+ ? " Completions may offer commands your CLI does not have."
222
+ : " Completions may be missing commands your CLI supports.",
223
+ " Update them with: npm install -g postman-cli-completion@latest",
224
+ );
225
+ } else if (vsSpec === 0) {
226
+ lines.push(`Completions match your installed CLI (${installed}).`);
227
+ }
228
+
229
+ return { level, lines, outdated, drifted };
230
+ }
231
+
232
+ /**
233
+ * Run both comparisons and build the report.
234
+ * `offline: true` skips the registry lookup entirely (no network, no delay).
235
+ */
236
+ export async function checkVersions({
237
+ specPath,
238
+ packageJsonPath,
239
+ offline = false,
240
+ command = "postman",
241
+ timeoutMs = 5000,
242
+ fetchImpl = globalThis.fetch,
243
+ } = {}) {
244
+ const spec = resolveTargetVersion({ specPath, packageJsonPath });
245
+ const [{ version: installed, error: installError }, latestResult] = await Promise.all([
246
+ detectInstalledVersion({ command, timeoutMs }),
247
+ offline
248
+ ? Promise.resolve({ version: null, error: "skipped (offline)" })
249
+ : fetchLatestVersion({ timeoutMs, fetchImpl }),
250
+ ]);
251
+
252
+ const report = buildReport({ installed, spec, latest: latestResult.version });
253
+ return {
254
+ ...report,
255
+ installed,
256
+ spec,
257
+ latest: latestResult.version,
258
+ installError,
259
+ latestError: latestResult.error,
260
+ };
261
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postman-cli-completion",
3
- "version": "1.71.0",
3
+ "version": "1.74.0",
4
4
  "description": "Shell completions for the Postman CLI (zsh, bash, fish)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
+ "lib/",
11
12
  "completions/zsh/_postman",
12
13
  "completions/bash/postman.bash",
13
14
  "completions/fish/postman.fish",
@@ -19,6 +20,7 @@
19
20
  "generate": "node scripts/generate.mjs",
20
21
  "generate:check": "node scripts/generate.mjs --check",
21
22
  "check-version": "node scripts/check-version.mjs",
23
+ "postinstall": "node lib/postinstall.mjs",
22
24
  "test": "node --test tests/*.test.mjs",
23
25
  "prepublishOnly": "node scripts/generate.mjs --check && node --test tests/*.test.mjs"
24
26
  },