@ladamczyk/outdated 1.0.0 → 1.1.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 CHANGED
@@ -10,7 +10,7 @@ outdated [options]
10
10
 
11
11
  The table mirrors `npm outdated` (`Package`, `Current`, `Wanted`, `Latest`, `Location`, `Depended by`) and adds `Last publish` and `Flags` at the end. It is split into `dependencies`, `devDependencies` and `optionalDependencies` sections (empty ones are omitted); packages keep their `package.json` order and the columns line up across sections. `Wanted` falls back to `Current` for packages `npm outdated` does not list.
12
12
 
13
- Flags are coloured: deprecated red, stale yellow, outdated cyan, unknown magenta, blocked blue, ok green; a major version bump shows `Latest` in red. Package names are links to their npmjs.com page in terminals that support hyperlinks. Colours and links turn off automatically when output is piped or `NO_COLOR` is set, and `--json` never contains either.
13
+ Flags are coloured: deprecated red, stale yellow, outdated cyan, unknown magenta, blocked blue, ok green; a major version bump shows `Latest` in red. The `Links` column holds `npm` and, when the package declares a repository, `repo`; each label is a hyperlink in terminals that support them. Colours and links turn off automatically when output is piped or `NO_COLOR` is set, and `--json` never contains either.
14
14
 
15
15
  ## Options
16
16
 
@@ -57,6 +57,7 @@ When `--json` is used, the output conforms to the following schema (schemaVersio
57
57
  "stale": true,
58
58
  "unknown": "reason npm view failed or null",
59
59
  "latestNode": "engines.node of the latest release, or null",
60
+ "repository": "https URL of the package repository, or null",
60
61
  "flags": ["deprecated", "stale", "outdated", "unknown", "blocked"]
61
62
  }
62
63
  ],
@@ -104,3 +105,23 @@ Note that `--only-problems` filters the table output but never filters the JSON
104
105
  **Windows behaviour is untested/unverified.**
105
106
 
106
107
  Only the npm CLI is used. Registry, proxy, authentication, and certificate settings are inherited from `.npmrc`.
108
+
109
+ ## Anonymous usage stats
110
+
111
+ Opt-in, off until a human says yes. The first interactive run asks; the answer is stored as `stats: true|false` in `~/.config/outdated.json` (or `$XDG_CONFIG_HOME/outdated.json`) and never asked again — edit or delete the key to change it. Runs that can't ask (`CI=true`, a pipe, `--json`) are never prompted and never counted.
112
+
113
+ A counted run posts one constant to `https://stats.adamczyk.ovh` and nothing else:
114
+
115
+ ```jsonc
116
+ { "tool": "outdated", "options": [] } // `options` is always empty
117
+ ```
118
+
119
+ Where an outbound POST never leaves the network, the same run counts as a plain image GET instead — `https://adamczyk.ovh/img/stats/pixel.png?tool=outdated`.
120
+
121
+ Never sent: your dependencies, package.json, paths, versions, findings, the flags you typed, project or package names, or anything identifying the user or machine. Sends are fire-and-forget with a 2s timeout; a failure is swallowed and never affects the exit code.
122
+
123
+ ## Last 12 months usage statistics
124
+
125
+ Regenerated on each release, for daily updated numbers click on chart or visit [https://adamczyk.ovh/docs/outdated#usage-statistics](https://adamczyk.ovh/docs/outdated#usage-statistics).
126
+
127
+ [![Last 12 months usage statistics](https://adamczyk.ovh/img/stats/outdated.png?date=2026-10-08)](https://adamczyk.ovh/img/stats/outdated.png)
package/bin/cli.js CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import{EExitCode as e,executeCommand as t}from"@ladamczyk/qoq-utils";import n from"cac";import{basename as r,join as i,resolve as a}from"path";import{readFileSync as o}from"fs";import s from"picocolors";var c=`1.0.0`;const l=[[`prod`,`dependencies`],[`optional`,`optionalDependencies`],[`dev`,`devDependencies`]],u=[`file:`,`link:`,`workspace:`,`git+`,`git:`,`github:`,`http:`,`https:`],d=/^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/,f=e=>{try{let t=JSON.parse(o(e,`utf8`));return typeof t==`object`&&t?t:null}catch{return null}},ee=e=>{if(!e.startsWith(`npm:`))return null;let t=e.slice(4),n=t.lastIndexOf(`@`);return n>0?t.slice(0,n):t},p=(e,t,n)=>{for(let[r,i]of l){let a=e[i];if(t.includes(r)&&typeof a==`object`&&a)for(let[e,t]of Object.entries(a))!n.has(e)&&typeof t==`string`&&n.set(e,{type:r,specifier:t})}};function te(e,{include:t,ignore:n}){let a=i(e,`package.json`),o=f(a);if(!o)throw Error(`Cannot read or parse package.json at ${a}`);let s=/* @__PURE__ */ new Map;p(o,t,s);let c=[],l=[];for(let[e,{type:t,specifier:r}]of s){if(n.includes(e))continue;let i=u.find(e=>r.startsWith(e));if(i){l.push({name:e,type:t,specifier:r,reason:`unsupported specifier (${i})`});continue}let a=ee(r);c.push({name:e,type:t,specifier:r,queriedName:a,validName:d.test(a??e)})}return{project:typeof o.name==`string`&&o.name?o.name:r(e),entries:c,skipped:l}}function ne(e,t){let n=f(i(e,`node_modules`,t,`package.json`))?.version;return typeof n==`string`?n:null}const m=e=>typeof e==`object`&&!!e&&!Array.isArray(e),h=e=>typeof e==`string`?e:null,g=e=>JSON.parse(e),re=e=>{let t=Array.isArray(e)?e[0]:e,n=m(t)?t:{};return{current:h(n.current),wanted:h(n.wanted),latest:h(n.latest)}},_=e=>!m(e)||e.code===void 0&&e.summary===void 0?null:[h(e.code),h(e.summary)].filter(Boolean).join(` `),v=e=>{if(e.trim()===``)throw Error(`npm outdated printed empty output`);let t;try{t=g(e)}catch{throw Error(`npm outdated printed invalid JSON`)}if(!m(t))throw Error(`npm outdated printed non-object JSON`);let n=_(t.error);if(n!==null)throw Error(`npm outdated failed: ${n}`);return Object.fromEntries(Object.entries(t).map(([e,t])=>[e,re(t)]))},ie=e=>m(e)&&typeof e.node==`string`&&e.node!==``?e.node:null,y={unknown:`unexpected npm view output`},ae=e=>{let t=e[`dist-tags`],n=m(t)?h(t.latest):null;if(n===null||!m(e.time))return y;let r=h(e.time[n]);if(r===null||Number.isNaN(Date.parse(r)))return{unknown:`no valid publish time for ${n}`};let i=e.deprecated;return{latest:n,lastPublish:r,deprecated:typeof i==`string`&&i!==``?i:null,latestNode:ie(e.engines)}},oe=e=>{let t;try{t=g(e)}catch{return{unknown:`invalid npm view JSON`}}return m(t)?`error`in t?{unknown:_(t.error)??`npm view error`}:ae(t):y};async function se(e,t,n){let r=Array(e.length),i=/* @__PURE__ */ new Set;for(let[a,o]of e.entries()){let e=n(o).then(t=>{r[a]=t,i.delete(e)});i.add(e),i.size===t&&await Promise.race(i)}return await Promise.all(i),r}const ce=String.raw`'\''`,le=e=>process.platform===`win32`?`"${e.replaceAll(`"`,`""`)}"`:`'${e.replaceAll(`'`,ce)}'`,ue=()=>async e=>{if(e.some(e=>/[\0\n\r]/.test(e)))throw Error(`npm argument contains a control character`);let n=await t([`npm`,...e.map(le)].join(` `),[],[`ignore`,`pipe`,`pipe`],!0);return String(n)};function b(e,t){let n=e.getUTCFullYear()*12+e.getUTCMonth()+t,r=Math.floor(n/12),i=n-r*12,a=new Date(Date.UTC(r,i+1,0)).getUTCDate(),o=new Date(e);return o.setUTCFullYear(r,i,Math.min(e.getUTCDate(),a)),o}function de(e,t){let n=new Date(e);if(n>t)return`0d ago`;let r=t.getTime()-n.getTime(),i=Math.floor(r/864e5);if(i<30)return`${i}d ago`;let a=0;for(;b(n,a+1)<=t;)a++;return a<12?`${a}m ago`:`${Math.floor(a/12)}y ago`}const fe=[`prod`,`dev`,`optional`],x=e=>{let t=/^v?(\d+)/.exec(e??``);return t?Number(t[1]):null},S=(e,t)=>{let n=x(e),r=x(t);return n!==null&&r!==null&&r>n},C=async(e,t)=>{let n,r=new Promise((e,r)=>{n=setTimeout(()=>{r(/* @__PURE__ */ Error(`npm view timed out after ${t}ms`))},t)});try{return await Promise.race([e,r])}finally{clearTimeout(n)}},w=async(e,t)=>{if(!e.validName)return{unknown:`invalid package name`};let n=[`view`,e.queriedName??e.name,`time`,`dist-tags`,`deprecated`,`engines`,`--json`,`--prefix`,t.cwd];try{return oe(await C(t.runner(n),t.timeoutMs))}catch(e){return{unknown:e instanceof Error?e.message:String(e)}}},T=e=>e.queriedName===null?e.name:`${e.name}:${e.specifier.replace(/^npm:/,``)}`,E=(e,t,n,r)=>{let i=n?n.latest:r;return t!==null&&t.latestNode!==null&&t.deprecated===null&&e!==null&&i!==null&&i!==e},D=(e,t)=>{let n=[];return e.deprecated!==null&&n.push(`deprecated`),e.stale&&n.push(`stale`),e.outdated&&n.push(`outdated`),e.unknown!==null&&n.push(`unknown`),t&&n.push(`blocked`),n.length>0?n:[`ok`]},O=(e,t,n,r)=>{let i=`latest`in t?t:null,a=n?.current??ne(r.cwd,e.name),o=i?.latest??n?.latest??null,s=i?new Date(i.lastPublish):null,c=n!==void 0,l={name:e.name,queriedName:e.queriedName,type:e.type,specifier:e.specifier,current:a,wanted:n?.wanted??null,latest:o,outdated:c,majorBump:c&&S(a,o),deprecated:i?.deprecated??null,latestNode:i?.latestNode??null,lastPublish:s?s.toISOString():null,stale:s!==null&&s<b(r.now,-r.staleAfterMonths),unknown:`unknown`in t?t.unknown:null};return{...l,flags:D(l,E(o,i,n,a))}},k=(e,t)=>{let n=t=>e.filter(e=>e.flags.includes(t)).length;return{total:e.length,deprecated:n(`deprecated`),stale:n(`stale`),outdated:n(`outdated`),unknown:n(`unknown`),blocked:n(`blocked`),ok:n(`ok`),skipped:t}},A=async(e={})=>{let t=a(e.cwd??process.cwd()),n=e.staleAfterMonths??6,r=e.now??/* @__PURE__ */ new Date,i=e.runner??ue(),{project:o,entries:s,skipped:c}=te(t,{include:e.include??fe,ignore:e.ignore??[]}),l=v(await i([`outdated`,`--json`,`--prefix`,t])),u={cwd:t,runner:i,timeoutMs:e.timeoutMs??3e4,now:r,staleAfterMonths:n,outdated:l},d=await se(s,e.concurrency??6,async e=>O(e,await w(e,u),l[T(e)],u));return{schemaVersion:1,generatedAt:r.toISOString(),staleAfterMonths:n,project:o,packages:d,summary:k(d,c.length),skipped:c.sort((e,t)=>e.name<t.name?-1:Number(e.name>t.name))}},j=[`Package`,`Current`,`Wanted`,`Latest`,`Location`,`Depended by`,`Last publish`,`Flags`],M=/* @__PURE__ */ new Set([1,2,3]),N=[[`prod`,`dependencies`],[`dev`,`devDependencies`],[`optional`,`optionalDependencies`]],P=e=>`${JSON.stringify(e,null,2)}\n`,F=e=>e.length>60?`${e.slice(0,59)}…`:e,I=(e,t)=>t===`deprecated`?`deprecated ${F(e.deprecated??``)}`:t===`blocked`?`latest needs node ${e.latestNode??``}`:t===`unknown`?`unknown ${e.unknown??``}`:t,L=(e,t)=>e.lastPublish===null?`-`:`${e.lastPublish.slice(0,10)} (${de(e.lastPublish,t)})`,R=(e,t,n)=>[e.name,e.current??`-`,e.wanted??e.current??`-`,e.latest??`-`,`node_modules/${e.name}`,t,L(e,n)],z=e=>({deprecated:e.red,stale:e.yellow,outdated:e.cyan,unknown:e.magenta,blocked:e.blue,ok:e.green}),B=(e,t)=>{let n=z(t);return e.flags.map(t=>n[t](I(e,t))).join(` `)},V=(e,t)=>`\u001b]8;;${e}\u001b\\${t}\u001b]8;;\u001b\\`,H=e=>`https://www.npmjs.com/package/${e.queriedName??e.name}`,U=e=>e,W=(e,t)=>e.outdated?e.current===e.wanted?t.yellow:t.red:U,pe=(e,t)=>e.majorBump?t.red:e.outdated?t.magenta:U,me=(e,t,n)=>[n?n=>V(H(e),W(e,t)(n)):W(e,t),U,e.outdated?t.green:U,pe(e,t),t.dim,t.dim,U],G=(e,t,n,r)=>{let i=` `.repeat(Math.max(0,t-e.length));return M.has(n)?i+r(e):r(e)+i},he=(e,t,n,r,i)=>[...me(e,r,i).map((e,r)=>G(t[r]??``,n[r]??0,r,e)),B(e,r)].join(` `).trimEnd(),ge=(e,t,n,r,i)=>{let a=e.map(e=>({pkg:e,cells:R(e,t,n)})),o=j.map((e,t)=>Math.max(e.length,...a.map(e=>e.cells[t]?.length??0))),s=j.map((e,t)=>t===j.length-1?e:G(e,o[t]??0,t,U)),c=[r.underline(r.bold(s.join(` `)))];for(let[e,t]of N){let n=a.filter(t=>t.pkg.type===e);n.length>0&&(c.push(``,r.bold(`${t} (${n.length})`)),c.push(...n.map(e=>he(e.pkg,e.cells,o,r,i))))}return c},_e=(e,t)=>{let n=e.summary;return[`${n.total} packages`,t.red(`${n.deprecated} deprecated`),t.yellow(`${n.stale} stale`),t.cyan(`${n.outdated} outdated`),t.magenta(`${n.unknown} unknown`),t.blue(`${n.blocked} blocked`),t.green(`${n.ok} ok`)].join(` · `)},ve=(e,t)=>{let n=t.color??s.isColorSupported,r=s.createColors(n),i=t.onlyProblems?e.packages.filter(e=>e.flags.length!==1||e.flags[0]!==`ok`):e.packages,a=e.packages.length===0?[`No dependencies found`]:ge(i,e.project,t.now,r,n);if(a.push(``,_e(e,r)),e.skipped.length>0){a.push(``,r.bold(`Skipped:`));let t=Math.max(...e.skipped.map(e=>e.name.length));for(let n of e.skipped)a.push(` ${n.name.padEnd(t)} ${n.specifier} — ${n.reason}`)}return`${a.join(`
3
- `)}\n`},K=[`prod`,`dev`,`optional`],ye=[`stale`,`deprecated`,`outdated`,`unknown`],be=/* @__PURE__ */ new Set([`--`,`cwd`,`staleAfter`,`stale-after`,`include`,`ignore`,`concurrency`,`json`,`onlyProblems`,`only-problems`,`failOn`,`fail-on`,`h`,`v`,`help`,`version`]),q=e=>Array.isArray(e)?e.map(q).join(`,`):typeof e==`string`||typeof e==`number`||typeof e==`boolean`?String(e):JSON.stringify(e)??`undefined`,J=(e,t,n)=>e[t]??e[n],Y=(e,t,n)=>{if(e===void 0)return n;let r=q(e);if(!/^[1-9]\d*$/.test(r))throw Error(`${t} must be a positive integer, got "${r}"`);return Number(r)},X=(e,t)=>{let n=q(e),r=n.split(`,`);if(typeof e==`boolean`||r.includes(``))throw Error(`${t} must be a comma-separated list without empty entries, got "${n}"`);return r},Z=(e,t,n,r)=>{if(e===void 0)return r;let i=X(e,t),a=i.find(e=>!n.includes(e));if(a!==void 0)throw Error(`${t} has invalid value "${a}"; allowed: ${n.join(`, `)}`);return i},Q=(e,t)=>{if(e===void 0)return!1;if(typeof e!=`boolean`)throw Error(`${t} is a switch and takes no value, got "${q(e)}"`);return e},xe=e=>{if(e!==void 0){if(typeof e!=`string`&&typeof e!=`number`)throw Error(`--cwd must be a single path, got "${q(e)}"`);return{cwd:String(e)}}},Se=e=>{let t=Object.keys(e).find(e=>!be.has(e));if(t!==void 0)throw Error(`Unknown option: ${t}`);let{ignore:n}=e;return{...xe(e.cwd),staleAfterMonths:Y(J(e,`staleAfter`,`stale-after`),`--stale-after`,6),include:Z(e.include,`--include`,K,K),ignore:n===void 0?[]:X(n,`--ignore`),concurrency:Y(e.concurrency,`--concurrency`,6),json:Q(e.json,`--json`),onlyProblems:Q(J(e,`onlyProblems`,`only-problems`),`--only-problems`),failOn:Z(J(e,`failOn`,`fail-on`),`--fail-on`,ye,[])}},Ce=(t,n)=>n.some(e=>t.summary[e]>0)?e.ERROR:e.OK;var $=class extends Error{};const we=()=>n(`outdated`).usage(`[options]`).help(e=>{throw new $(e.map(e=>e.title?`${e.title}:\n${e.body}`:e.body).join(`
2
+ import{EExitCode as e,PIXEL_URL as t,STATS_URL as n,executeCommand as r,sendStats as i}from"@ladamczyk/qoq-utils";import a from"cac";import{basename as o,dirname as s,join as c,resolve as l}from"path";import{mkdirSync as u,readFileSync as d,writeFileSync as f}from"fs";import p from"picocolors";import{homedir as m}from"os";import{createInterface as h}from"readline/promises";var ee=`1.0.0`;const te=[[`prod`,`dependencies`],[`optional`,`optionalDependencies`],[`dev`,`devDependencies`]],ne=[`file:`,`link:`,`workspace:`,`git+`,`git:`,`github:`,`http:`,`https:`],re=/^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/,g=e=>{try{let t=JSON.parse(d(e,`utf8`));return typeof t==`object`&&t?t:null}catch{return null}},ie=e=>{if(!e.startsWith(`npm:`))return null;let t=e.slice(4),n=t.lastIndexOf(`@`);return n>0?t.slice(0,n):t},ae=(e,t,n)=>{for(let[r,i]of te){let a=e[i];if(t.includes(r)&&typeof a==`object`&&a)for(let[e,t]of Object.entries(a))!n.has(e)&&typeof t==`string`&&n.set(e,{type:r,specifier:t})}};function oe(e,{include:t,ignore:n}){let r=c(e,`package.json`),i=g(r);if(!i)throw Error(`Cannot read or parse package.json at ${r}`);let a=/* @__PURE__ */ new Map;ae(i,t,a);let s=[],l=[];for(let[e,{type:t,specifier:r}]of a){if(n.includes(e))continue;let i=ne.find(e=>r.startsWith(e));if(i){l.push({name:e,type:t,specifier:r,reason:`unsupported specifier (${i})`});continue}let a=ie(r);s.push({name:e,type:t,specifier:r,queriedName:a,validName:re.test(a??e)})}return{project:typeof i.name==`string`&&i.name?i.name:o(e),entries:s,skipped:l}}function se(e,t){let n=g(c(e,`node_modules`,t,`package.json`))?.version;return typeof n==`string`?n:null}const _=e=>typeof e==`object`&&!!e&&!Array.isArray(e),v=e=>typeof e==`string`?e:null,y=e=>JSON.parse(e),ce=e=>{let t=Array.isArray(e)?e[0]:e,n=_(t)?t:{};return{current:v(n.current),wanted:v(n.wanted),latest:v(n.latest)}},b=e=>!_(e)||e.code===void 0&&e.summary===void 0?null:[v(e.code),v(e.summary)].filter(Boolean).join(` `),le=e=>{if(e.trim()===``)throw Error(`npm outdated printed empty output`);let t;try{t=y(e)}catch{throw Error(`npm outdated printed invalid JSON`)}if(!_(t))throw Error(`npm outdated printed non-object JSON`);let n=b(t.error);if(n!==null)throw Error(`npm outdated failed: ${n}`);return Object.fromEntries(Object.entries(t).map(([e,t])=>[e,ce(t)]))},ue=e=>_(e)&&typeof e.node==`string`&&e.node!==``?e.node:null,de=e=>{let t=_(e)?e.url:e;if(typeof t!=`string`||t===``)return null;let n=t.replace(/^git\+/,``).replace(/^(?:git|ssh):\/\/(?:git@)?/,`https://`).replace(/^git@([^:]+):/,`https://$1/`).replace(/^(?:github:)?([\w.-]+\/[\w.-]+)$/,`https://github.com/$1`).replace(/\.git$/,``);return/^https?:\/\//.test(n)?n:null},x={unknown:`unexpected npm view output`},fe=e=>{let t=e[`dist-tags`],n=_(t)?v(t.latest):null;if(n===null||!_(e.time))return x;let r=v(e.time[n]);if(r===null||Number.isNaN(Date.parse(r)))return{unknown:`no valid publish time for ${n}`};let i=e.deprecated;return{latest:n,lastPublish:r,deprecated:typeof i==`string`&&i!==``?i:null,latestNode:ue(e.engines),repository:de(e.repository)}},pe=e=>{let t;try{t=y(e)}catch{return{unknown:`invalid npm view JSON`}}return _(t)?`error`in t?{unknown:b(t.error)??`npm view error`}:fe(t):x};async function S(e,t,n){let r=Array(e.length),i=/* @__PURE__ */ new Set;for(let[a,o]of e.entries()){let e=n(o).then(t=>{r[a]=t,i.delete(e)});i.add(e),i.size===t&&await Promise.race(i)}return await Promise.all(i),r}const C=String.raw`'\''`,w=e=>process.platform===`win32`?`"${e.replaceAll(`"`,`""`)}"`:`'${e.replaceAll(`'`,C)}'`,T=()=>async e=>{if(e.some(e=>/[\0\n\r]/.test(e)))throw Error(`npm argument contains a control character`);let t=await r([`npm`,...e.map(w)].join(` `),[],[`ignore`,`pipe`,`pipe`],!0);return String(t)};function E(e,t){let n=e.getUTCFullYear()*12+e.getUTCMonth()+t,r=Math.floor(n/12),i=n-r*12,a=new Date(Date.UTC(r,i+1,0)).getUTCDate(),o=new Date(e);return o.setUTCFullYear(r,i,Math.min(e.getUTCDate(),a)),o}function D(e,t){let n=new Date(e);if(n>t)return`0d ago`;let r=t.getTime()-n.getTime(),i=Math.floor(r/864e5);if(i<30)return`${i}d ago`;let a=0;for(;E(n,a+1)<=t;)a++;return a<12?`${a}m ago`:`${Math.floor(a/12)}y ago`}const O=[`prod`,`dev`,`optional`],k=e=>{let t=/^v?(\d+)/.exec(e??``);return t?Number(t[1]):null},A=(e,t)=>{let n=k(e),r=k(t);return n!==null&&r!==null&&r>n},j=async(e,t)=>{let n,r=new Promise((e,r)=>{n=setTimeout(()=>{r(/* @__PURE__ */ Error(`npm view timed out after ${t}ms`))},t)});try{return await Promise.race([e,r])}finally{clearTimeout(n)}},M=async(e,t)=>{if(!e.validName)return{unknown:`invalid package name`};let n=[`view`,e.queriedName??e.name,`time`,`dist-tags`,`deprecated`,`engines`,`repository`,`--json`,`--prefix`,t.cwd];try{return pe(await j(t.runner(n),t.timeoutMs))}catch(e){return{unknown:e instanceof Error?e.message:String(e)}}},N=e=>e.queriedName===null?e.name:`${e.name}:${e.specifier.replace(/^npm:/,``)}`,P=(e,t,n,r)=>{let i=n?n.latest:r;return t!==null&&t.latestNode!==null&&t.deprecated===null&&e!==null&&i!==null&&i!==e},F=(e,t)=>{let n=[];return e.deprecated!==null&&n.push(`deprecated`),e.stale&&n.push(`stale`),e.outdated&&n.push(`outdated`),e.unknown!==null&&n.push(`unknown`),t&&n.push(`blocked`),n.length>0?n:[`ok`]},I=(e,t,n,r)=>{let i=`latest`in t?t:null,a=n?.current??se(r.cwd,e.name),o=i?.latest??n?.latest??null,s=i?new Date(i.lastPublish):null,c=n!==void 0,l={name:e.name,queriedName:e.queriedName,type:e.type,specifier:e.specifier,current:a,wanted:n?.wanted??null,latest:o,outdated:c,majorBump:c&&A(a,o),deprecated:i?.deprecated??null,latestNode:i?.latestNode??null,repository:i?.repository??null,lastPublish:s?s.toISOString():null,stale:s!==null&&s<E(r.now,-r.staleAfterMonths),unknown:`unknown`in t?t.unknown:null};return{...l,flags:F(l,P(o,i,n,a))}},L=(e,t)=>{let n=t=>e.filter(e=>e.flags.includes(t)).length;return{total:e.length,deprecated:n(`deprecated`),stale:n(`stale`),outdated:n(`outdated`),unknown:n(`unknown`),blocked:n(`blocked`),ok:n(`ok`),skipped:t}},R=async(e={})=>{let t=l(e.cwd??process.cwd()),n=e.staleAfterMonths??6,r=e.now??/* @__PURE__ */ new Date,i=e.runner??T(),{project:a,entries:o,skipped:s}=oe(t,{include:e.include??O,ignore:e.ignore??[]}),c=le(await i([`outdated`,`--json`,`--prefix`,t])),u={cwd:t,runner:i,timeoutMs:e.timeoutMs??3e4,now:r,staleAfterMonths:n,outdated:c},d=await S(o,e.concurrency??6,async e=>I(e,await M(e,u),c[N(e)],u));return{schemaVersion:1,generatedAt:r.toISOString(),staleAfterMonths:n,project:a,packages:d,summary:L(d,s.length),skipped:s.sort((e,t)=>e.name<t.name?-1:Number(e.name>t.name))}},z=[`Package`,`Current`,`Wanted`,`Latest`,`Location`,`Depended by`,`Last publish`,`Links`,`Flags`],B=/* @__PURE__ */ new Set([1,2,3]),me=[[`prod`,`dependencies`],[`dev`,`devDependencies`],[`optional`,`optionalDependencies`]],he=e=>`${JSON.stringify(e,null,2)}\n`,ge=e=>e.length>60?`${e.slice(0,59)}…`:e,_e=(e,t)=>t===`deprecated`?`deprecated ${ge(e.deprecated??``)}`:t===`blocked`?`latest needs node ${e.latestNode??``}`:t===`unknown`?`unknown ${e.unknown??``}`:t,ve=(e,t)=>e.lastPublish===null?`-`:`${e.lastPublish.slice(0,10)} (${D(e.lastPublish,t)})`,ye=e=>e.repository===null?`npm`:`npm / repo`,be=(e,t,n)=>[e.name,e.current??`-`,e.wanted??e.current??`-`,e.latest??`-`,`node_modules/${e.name}`,t,ve(e,n),ye(e)],xe=e=>({deprecated:e.red,stale:e.yellow,outdated:e.cyan,unknown:e.magenta,blocked:e.blue,ok:e.green}),Se=(e,t)=>{let n=xe(t);return e.flags.map(t=>n[t](_e(e,t))).join(` `)},Ce=(e,t)=>`\u001b]8;;${e}\u001b\\${t}\u001b]8;;\u001b\\`,we=e=>`https://www.npmjs.com/package/${e.queriedName??e.name}`,V=e=>e,Te=(e,t)=>e.outdated?e.current===e.wanted?t.yellow:t.red:V,Ee=(e,t)=>e.majorBump?t.red:e.outdated?t.magenta:V,De=e=>t=>t.split(` / `).map((t,n)=>Ce(n===0?we(e):e.repository??``,t)).join(` / `),Oe=(e,t,n)=>[Te(e,t),V,e.outdated?t.green:V,Ee(e,t),t.dim,t.dim,V,n?De(e):V],H=(e,t,n,r)=>{let i=` `.repeat(Math.max(0,t-e.length));return B.has(n)?i+r(e):r(e)+i},ke=(e,t,n,r,i)=>[...Oe(e,r,i).map((e,r)=>H(t[r]??``,n[r]??0,r,e)),Se(e,r)].join(` `).trimEnd(),Ae=(e,t,n,r,i)=>{let a=e.map(e=>({pkg:e,cells:be(e,t,n)})),o=z.map((e,t)=>Math.max(e.length,...a.map(e=>e.cells[t]?.length??0))),s=z.map((e,t)=>t===z.length-1?e:H(e,o[t]??0,t,V)),c=[r.underline(r.bold(s.join(` `)))];for(let[e,t]of me){let n=a.filter(t=>t.pkg.type===e);n.length>0&&(c.push(``,r.bold(`${t} (${n.length})`)),c.push(...n.map(e=>ke(e.pkg,e.cells,o,r,i))))}return c},je=(e,t)=>{let n=e.summary;return[`${n.total} packages`,t.red(`${n.deprecated} deprecated`),t.yellow(`${n.stale} stale`),t.cyan(`${n.outdated} outdated`),t.magenta(`${n.unknown} unknown`),t.blue(`${n.blocked} blocked`),t.green(`${n.ok} ok`)].join(` · `)},Me=(e,t)=>{let n=t.color??p.isColorSupported,r=p.createColors(n),i=t.onlyProblems?e.packages.filter(e=>e.flags.length!==1||e.flags[0]!==`ok`):e.packages,a=e.packages.length===0?[`No dependencies found`]:Ae(i,e.project,t.now,r,n);if(a.push(``,je(e,r)),e.skipped.length>0){a.push(``,r.bold(`Skipped:`));let t=Math.max(...e.skipped.map(e=>e.name.length));for(let n of e.skipped)a.push(` ${n.name.padEnd(t)} ${n.specifier} — ${n.reason}`)}return`${a.join(`
3
+ `)}\n`},U=[`prod`,`dev`,`optional`],Ne=[`stale`,`deprecated`,`outdated`,`unknown`],Pe=/* @__PURE__ */ new Set([`--`,`cwd`,`staleAfter`,`stale-after`,`include`,`ignore`,`concurrency`,`json`,`onlyProblems`,`only-problems`,`failOn`,`fail-on`,`h`,`v`,`help`,`version`]),W=e=>Array.isArray(e)?e.map(W).join(`,`):typeof e==`string`||typeof e==`number`||typeof e==`boolean`?String(e):JSON.stringify(e)??`undefined`,G=(e,t,n)=>e[t]??e[n],K=(e,t,n)=>{if(e===void 0)return n;let r=W(e);if(!/^[1-9]\d*$/.test(r))throw Error(`${t} must be a positive integer, got "${r}"`);return Number(r)},q=(e,t)=>{let n=W(e),r=n.split(`,`);if(typeof e==`boolean`||r.includes(``))throw Error(`${t} must be a comma-separated list without empty entries, got "${n}"`);return r},J=(e,t,n,r)=>{if(e===void 0)return r;let i=q(e,t),a=i.find(e=>!n.includes(e));if(a!==void 0)throw Error(`${t} has invalid value "${a}"; allowed: ${n.join(`, `)}`);return i},Y=(e,t)=>{if(e===void 0)return!1;if(typeof e!=`boolean`)throw Error(`${t} is a switch and takes no value, got "${W(e)}"`);return e},Fe=e=>{if(e!==void 0){if(typeof e!=`string`&&typeof e!=`number`)throw Error(`--cwd must be a single path, got "${W(e)}"`);return{cwd:String(e)}}},Ie=e=>{let t=Object.keys(e).find(e=>!Pe.has(e));if(t!==void 0)throw Error(`Unknown option: ${t}`);let{ignore:n}=e;return{...Fe(e.cwd),staleAfterMonths:K(G(e,`staleAfter`,`stale-after`),`--stale-after`,6),include:J(e.include,`--include`,U,U),ignore:n===void 0?[]:q(n,`--ignore`),concurrency:K(e.concurrency,`--concurrency`,6),json:Y(e.json,`--json`),onlyProblems:Y(G(e,`onlyProblems`,`only-problems`),`--only-problems`),failOn:J(G(e,`failOn`,`fail-on`),`--fail-on`,Ne,[])}},Le=(t,n)=>n.some(e=>t.summary[e]>0)?e.ERROR:e.OK,X=c(process.env.XDG_CONFIG_HOME??c(m(),`.config`),`outdated.json`),Z=e=>{try{let t=JSON.parse(d(e,`utf8`));return typeof t==`object`&&t?t:{}}catch{return{}}},Re=(e,t)=>{try{u(s(e),{recursive:!0}),f(e,`${JSON.stringify({...Z(e),stats:t},null,2)}\n`)}catch{}},ze=async()=>{process.stdout.write([p.bold(`
4
+ Outdated usage stats
5
+ `),`Send a count of outdated runs to ${n}? Each run posts one thing:\n`,` • the tool name — always the literal ${p.cyan(`"outdated"`)}\n`,p.gray(`Blocked POST? The same values go to ${t} as a GET.\n`),p.gray(`Never sent: your dependencies, package.json, paths, versions, findings,
6
+ the flags you typed, project or package names, and nothing identifying
7
+ you or your machine.
8
+ `),p.gray(`Stored as \`stats: true|false\` in ${X} — edit it any time.\n\n`)].join(``));let e=h({input:process.stdin,output:process.stdout});try{return(await e.question(`Send anonymous usage stats? [y/N] `)).trim().toLowerCase()===`y`}finally{e.close()}},Be=async(e=X)=>{let t=Z(e).stats;if(typeof t==`boolean`)return t;if(process.env.CI===`true`||!process.stdin.isTTY||!process.stdout.isTTY)return;let n=await ze();return Re(e,n),n},Ve=async()=>i(`outdated`);var Q=class extends Error{};const He=()=>a(`outdated`).usage(`[options]`).help(e=>{throw new Q(e.map(e=>e.title?`${e.title}:\n${e.body}`:e.body).join(`
4
9
 
5
- `))}).option(`-v, --version`,`Display version number`).option(`--cwd <dir>`,`Project folder (default: current directory)`).option(`--stale-after <months>`,`Months without a release before a package is stale (default: 6)`).option(`--include <types>`,`Dependency types to check: prod,dev,optional (default: all)`).option(`--ignore <pkgs>`,`Comma-separated package names to skip`).option(`--concurrency <n>`,`Parallel npm view lookups (default: 6)`).option(`--json`,`Output machine-readable JSON`).option(`--only-problems`,`Hide ok packages from the table`).option(`--fail-on <list>`,`Exit 1 when any of stale,deprecated,outdated,unknown is found`),Te=async(t,n,r)=>{let{options:i}=we().parse([`node`,`outdated`,...t],{run:!1});if(i.version)return{stdout:`${c}\n`,code:e.OK};let a=Se(i),o=await A({cwd:a.cwd,staleAfterMonths:a.staleAfterMonths,include:a.include,ignore:a.ignore,concurrency:a.concurrency,runner:n,now:r});return{stdout:a.json?P(o):ve(o,{onlyProblems:a.onlyProblems,now:r}),code:Ce(o,a.failOn)}},{stdout:Ee,stderr:De,code:Oe}=await(async(t,n={})=>{try{return{...await Te(t,n.runner,n.now??/* @__PURE__ */ new Date),stderr:``}}catch(t){return t instanceof $?{stdout:`${t.message}\n`,stderr:``,code:e.OK}:{stdout:``,stderr:`✖ ${t instanceof Error?t.message:String(t)}\n`,code:e.EXCEPTION}}})(process.argv.slice(2));process.stderr.write(De),process.stdout.write(Ee,()=>process.exit(Oe));
10
+ `))}).option(`-v, --version`,`Display version number`).option(`--cwd <dir>`,`Project folder (default: current directory)`).option(`--stale-after <months>`,`Months without a release before a package is stale (default: 6)`).option(`--include <types>`,`Dependency types to check: prod,dev,optional (default: all)`).option(`--ignore <pkgs>`,`Comma-separated package names to skip`).option(`--concurrency <n>`,`Parallel npm view lookups (default: 6)`).option(`--json`,`Output machine-readable JSON`).option(`--only-problems`,`Hide ok packages from the table`).option(`--fail-on <list>`,`Exit 1 when any of stale,deprecated,outdated,unknown is found`),Ue=async(t,n,r)=>{let{options:i}=He().parse([`node`,`outdated`,...t],{run:!1});if(i.version)return{stdout:`${ee}\n`,code:e.OK};let a=Ie(i);!a.json&&await Be()&&Ve();let o=await R({cwd:a.cwd,staleAfterMonths:a.staleAfterMonths,include:a.include,ignore:a.ignore,concurrency:a.concurrency,runner:n,now:r});return{stdout:a.json?he(o):Me(o,{onlyProblems:a.onlyProblems,now:r}),code:Le(o,a.failOn)}},{stdout:$,stderr:We,code:Ge}=await(async(t,n={})=>{try{return{...await Ue(t,n.runner,n.now??/* @__PURE__ */ new Date),stderr:``}}catch(t){return t instanceof Q?{stdout:`${t.message}\n`,stderr:``,code:e.OK}:{stdout:``,stderr:`✖ ${t instanceof Error?t.message:String(t)}\n`,code:e.EXCEPTION}}})(process.argv.slice(2));process.stderr.write(We),process.stdout.write($,()=>process.exit(Ge));
package/lib/index.cjs CHANGED
@@ -114,6 +114,12 @@ const parseOutdated = (stdout) => {
114
114
  return Object.fromEntries(Object.entries(parsed).map(([name, v]) => [name, toEntry(v)]));
115
115
  };
116
116
  const nodeRange = (engines) => isRecord(engines) && typeof engines.node === "string" && engines.node !== "" ? engines.node : null;
117
+ const repoUrl = (repository) => {
118
+ const raw = isRecord(repository) ? repository.url : repository;
119
+ if (typeof raw !== "string" || raw === "") return null;
120
+ const url = raw.replace(/^git\+/, "").replace(/^(?:git|ssh):\/\/(?:git@)?/, "https://").replace(/^git@([^:]+):/, "https://$1/").replace(/^(?:github:)?([\w.-]+\/[\w.-]+)$/, "https://github.com/$1").replace(/\.git$/, "");
121
+ return /^https?:\/\//.test(url) ? url : null;
122
+ };
117
123
  const UNEXPECTED = { unknown: "unexpected npm view output" };
118
124
  const buildView = (parsed) => {
119
125
  const tags = parsed["dist-tags"];
@@ -126,7 +132,8 @@ const buildView = (parsed) => {
126
132
  latest,
127
133
  lastPublish,
128
134
  deprecated: typeof dep === "string" && dep !== "" ? dep : null,
129
- latestNode: nodeRange(parsed.engines)
135
+ latestNode: nodeRange(parsed.engines),
136
+ repository: repoUrl(parsed.repository)
130
137
  };
131
138
  };
132
139
  const parseView = (stdout) => {
@@ -218,6 +225,7 @@ const lookup = async (entry, ctx) => {
218
225
  "dist-tags",
219
226
  "deprecated",
220
227
  "engines",
228
+ "repository",
221
229
  "--json",
222
230
  "--prefix",
223
231
  ctx.cwd
@@ -260,6 +268,7 @@ const buildRow = (entry, view, hit, ctx) => {
260
268
  majorBump: outdated && isMajorBump(current, latest),
261
269
  deprecated: viewed?.deprecated ?? null,
262
270
  latestNode: viewed?.latestNode ?? null,
271
+ repository: viewed?.repository ?? null,
263
272
  lastPublish: published ? published.toISOString() : null,
264
273
  stale: published !== null && published < addMonths(ctx.now, -ctx.staleAfterMonths),
265
274
  unknown: "unknown" in view ? view.unknown : null
package/lib/index.mjs CHANGED
@@ -113,6 +113,12 @@ const parseOutdated = (stdout) => {
113
113
  return Object.fromEntries(Object.entries(parsed).map(([name, v]) => [name, toEntry(v)]));
114
114
  };
115
115
  const nodeRange = (engines) => isRecord(engines) && typeof engines.node === "string" && engines.node !== "" ? engines.node : null;
116
+ const repoUrl = (repository) => {
117
+ const raw = isRecord(repository) ? repository.url : repository;
118
+ if (typeof raw !== "string" || raw === "") return null;
119
+ const url = raw.replace(/^git\+/, "").replace(/^(?:git|ssh):\/\/(?:git@)?/, "https://").replace(/^git@([^:]+):/, "https://$1/").replace(/^(?:github:)?([\w.-]+\/[\w.-]+)$/, "https://github.com/$1").replace(/\.git$/, "");
120
+ return /^https?:\/\//.test(url) ? url : null;
121
+ };
116
122
  const UNEXPECTED = { unknown: "unexpected npm view output" };
117
123
  const buildView = (parsed) => {
118
124
  const tags = parsed["dist-tags"];
@@ -125,7 +131,8 @@ const buildView = (parsed) => {
125
131
  latest,
126
132
  lastPublish,
127
133
  deprecated: typeof dep === "string" && dep !== "" ? dep : null,
128
- latestNode: nodeRange(parsed.engines)
134
+ latestNode: nodeRange(parsed.engines),
135
+ repository: repoUrl(parsed.repository)
129
136
  };
130
137
  };
131
138
  const parseView = (stdout) => {
@@ -217,6 +224,7 @@ const lookup = async (entry, ctx) => {
217
224
  "dist-tags",
218
225
  "deprecated",
219
226
  "engines",
227
+ "repository",
220
228
  "--json",
221
229
  "--prefix",
222
230
  ctx.cwd
@@ -259,6 +267,7 @@ const buildRow = (entry, view, hit, ctx) => {
259
267
  majorBump: outdated && isMajorBump(current, latest),
260
268
  deprecated: viewed?.deprecated ?? null,
261
269
  latestNode: viewed?.latestNode ?? null,
270
+ repository: viewed?.repository ?? null,
262
271
  lastPublish: published ? published.toISOString() : null,
263
272
  stale: published !== null && published < addMonths(ctx.now, -ctx.staleAfterMonths),
264
273
  unknown: "unknown" in view ? view.unknown : null
package/lib/src/npm.d.ts CHANGED
@@ -8,6 +8,7 @@ export type TViewResult = {
8
8
  lastPublish: string;
9
9
  deprecated: string | null;
10
10
  latestNode: string | null;
11
+ repository: string | null;
11
12
  } | {
12
13
  unknown: string;
13
14
  };
@@ -0,0 +1,2 @@
1
+ export declare const resolveConsent: (file?: string) => Promise<boolean | undefined>;
2
+ export declare const sendStats: () => Promise<void>;
@@ -0,0 +1 @@
1
+ export {};
@@ -24,6 +24,7 @@ export interface IPackageResult {
24
24
  majorBump: boolean;
25
25
  deprecated: string | null;
26
26
  latestNode: string | null;
27
+ repository: string | null;
27
28
  lastPublish: string | null;
28
29
  stale: boolean;
29
30
  unknown: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ladamczyk/outdated",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CLI that reports outdated dependencies",
5
5
  "keywords": [
6
6
  "cli",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@ladamczyk/qoq-utils": "6.1.10",
54
- "cac": "7.0.0",
54
+ "cac": "7.0.1",
55
55
  "picocolors": "1.1.1"
56
56
  },
57
57
  "devDependencies": {