@ladamczyk/outdated 1.0.0 → 1.1.1

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.1.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
@@ -1,7 +1,31 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
2
24
  let path = require("path");
3
25
  let fs = require("fs");
4
26
  let _ladamczyk_qoq_utils = require("@ladamczyk/qoq-utils");
27
+ let picocolors = require("picocolors");
28
+ picocolors = __toESM(picocolors, 1);
5
29
  //#region src/deps.ts
6
30
  const FIELDS = [
7
31
  ["prod", "dependencies"],
@@ -114,6 +138,12 @@ const parseOutdated = (stdout) => {
114
138
  return Object.fromEntries(Object.entries(parsed).map(([name, v]) => [name, toEntry(v)]));
115
139
  };
116
140
  const nodeRange = (engines) => isRecord(engines) && typeof engines.node === "string" && engines.node !== "" ? engines.node : null;
141
+ const repoUrl = (repository) => {
142
+ const raw = isRecord(repository) ? repository.url : repository;
143
+ if (typeof raw !== "string" || raw === "") return null;
144
+ const url = raw.replace(/^git\+/, "").replace(/^(?:git|ssh):\/\/(?:git@)?/, "https://").replace(/^git@([^:]+):/, "https://$1/").replace(/^(?:github:)?([\w.-]+\/[\w.-]+)$/, "https://github.com/$1").replace(/\.git$/, "");
145
+ return /^https?:\/\//.test(url) ? url : null;
146
+ };
117
147
  const UNEXPECTED = { unknown: "unexpected npm view output" };
118
148
  const buildView = (parsed) => {
119
149
  const tags = parsed["dist-tags"];
@@ -126,7 +156,8 @@ const buildView = (parsed) => {
126
156
  latest,
127
157
  lastPublish,
128
158
  deprecated: typeof dep === "string" && dep !== "" ? dep : null,
129
- latestNode: nodeRange(parsed.engines)
159
+ latestNode: nodeRange(parsed.engines),
160
+ repository: repoUrl(parsed.repository)
130
161
  };
131
162
  };
132
163
  const parseView = (stdout) => {
@@ -180,6 +211,17 @@ function addMonths(date, months) {
180
211
  result.setUTCFullYear(year, month, Math.min(date.getUTCDate(), lastDay));
181
212
  return result;
182
213
  }
214
+ function relativeAge(iso, now) {
215
+ const date = new Date(iso);
216
+ if (date > now) return "0d ago";
217
+ const diffMs = now.getTime() - date.getTime();
218
+ const days = Math.floor(diffMs / 864e5);
219
+ if (days < 30) return `${days}d ago`;
220
+ let months = 0;
221
+ while (addMonths(date, months + 1) <= now) months++;
222
+ if (months < 12) return `${months}m ago`;
223
+ return `${Math.floor(months / 12)}y ago`;
224
+ }
183
225
  //#endregion
184
226
  //#region src/check.ts
185
227
  const ALL_TYPES = [
@@ -218,6 +260,7 @@ const lookup = async (entry, ctx) => {
218
260
  "dist-tags",
219
261
  "deprecated",
220
262
  "engines",
263
+ "repository",
221
264
  "--json",
222
265
  "--prefix",
223
266
  ctx.cwd
@@ -260,6 +303,7 @@ const buildRow = (entry, view, hit, ctx) => {
260
303
  majorBump: outdated && isMajorBump(current, latest),
261
304
  deprecated: viewed?.deprecated ?? null,
262
305
  latestNode: viewed?.latestNode ?? null,
306
+ repository: viewed?.repository ?? null,
263
307
  lastPublish: published ? published.toISOString() : null,
264
308
  stale: published !== null && published < addMonths(ctx.now, -ctx.staleAfterMonths),
265
309
  unknown: "unknown" in view ? view.unknown : null
@@ -317,4 +361,133 @@ const check = async (options = {}) => {
317
361
  };
318
362
  };
319
363
  //#endregion
364
+ //#region src/format.ts
365
+ const HEADER = [
366
+ "Package",
367
+ "Current",
368
+ "Wanted",
369
+ "Latest",
370
+ "Location",
371
+ "Depended by",
372
+ "Last publish",
373
+ "Links",
374
+ "Flags"
375
+ ];
376
+ const RIGHT_ALIGNED = /* @__PURE__ */ new Set([
377
+ 1,
378
+ 2,
379
+ 3
380
+ ]);
381
+ const SECTIONS = [
382
+ ["prod", "dependencies"],
383
+ ["dev", "devDependencies"],
384
+ ["optional", "optionalDependencies"]
385
+ ];
386
+ const MAX_MESSAGE = 60;
387
+ const formatJson = (result) => `${JSON.stringify(result, null, 2)}\n`;
388
+ const truncate = (text) => text.length > MAX_MESSAGE ? `${text.slice(0, 59)}…` : text;
389
+ const flagText = (pkg, flag) => {
390
+ if (flag === "deprecated") return `deprecated ${truncate(pkg.deprecated ?? "")}`;
391
+ if (flag === "blocked") return `latest needs node ${pkg.latestNode ?? ""}`;
392
+ if (flag === "unknown") return `unknown ${pkg.unknown ?? ""}`;
393
+ return flag;
394
+ };
395
+ const publishText = (pkg, now) => pkg.lastPublish === null ? "-" : `${pkg.lastPublish.slice(0, 10)} (${relativeAge(pkg.lastPublish, now)})`;
396
+ const linksText = (pkg) => pkg.repository === null ? "npm" : "npm / repo";
397
+ const toCells = (pkg, project, now) => [
398
+ pkg.name,
399
+ pkg.current ?? "-",
400
+ pkg.wanted ?? pkg.current ?? "-",
401
+ pkg.latest ?? "-",
402
+ `node_modules/${pkg.name}`,
403
+ project,
404
+ publishText(pkg, now),
405
+ linksText(pkg)
406
+ ];
407
+ const flagColors = (c) => ({
408
+ deprecated: c.red,
409
+ stale: c.yellow,
410
+ outdated: c.cyan,
411
+ unknown: c.magenta,
412
+ blocked: c.blue,
413
+ ok: c.green
414
+ });
415
+ const flagsCell = (pkg, c) => {
416
+ const colors = flagColors(c);
417
+ return pkg.flags.map((flag) => colors[flag](flagText(pkg, flag))).join(" ");
418
+ };
419
+ const hyperlink = (url, text) => `\u001b]8;;${url}\u001b\\${text}\u001b]8;;\u001b\\`;
420
+ const npmUrl = (pkg) => `https://www.npmjs.com/package/${pkg.queriedName ?? pkg.name}`;
421
+ const plain = (text) => text;
422
+ const nameColor = (pkg, c) => {
423
+ if (!pkg.outdated) return plain;
424
+ return pkg.current === pkg.wanted ? c.yellow : c.red;
425
+ };
426
+ const latestColor = (pkg, c) => {
427
+ if (pkg.majorBump) return c.red;
428
+ return pkg.outdated ? c.magenta : plain;
429
+ };
430
+ const linkPaint = (pkg) => (text) => text.split(" / ").map((label, i) => hyperlink(i === 0 ? npmUrl(pkg) : pkg.repository ?? "", label)).join(" / ");
431
+ const paints = (pkg, c, links) => [
432
+ nameColor(pkg, c),
433
+ plain,
434
+ pkg.outdated ? c.green : plain,
435
+ latestColor(pkg, c),
436
+ c.dim,
437
+ c.dim,
438
+ plain,
439
+ links ? linkPaint(pkg) : plain
440
+ ];
441
+ const renderCell = (text, width, column, paint) => {
442
+ const gap = " ".repeat(Math.max(0, width - text.length));
443
+ return RIGHT_ALIGNED.has(column) ? gap + paint(text) : paint(text) + gap;
444
+ };
445
+ const renderRow = (pkg, cells, widths, c, links) => {
446
+ return [...paints(pkg, c, links).map((paint, i) => renderCell(cells[i] ?? "", widths[i] ?? 0, i, paint)), flagsCell(pkg, c)].join(" ").trimEnd();
447
+ };
448
+ const renderTable = (packages, project, now, c, links) => {
449
+ const rows = packages.map((pkg) => ({
450
+ pkg,
451
+ cells: toCells(pkg, project, now)
452
+ }));
453
+ const widths = HEADER.map((h, i) => Math.max(h.length, ...rows.map((r) => r.cells[i]?.length ?? 0)));
454
+ const header = HEADER.map((h, i) => i === HEADER.length - 1 ? h : renderCell(h, widths[i] ?? 0, i, plain));
455
+ const lines = [c.underline(c.bold(header.join(" ")))];
456
+ for (const [type, title] of SECTIONS) {
457
+ const section = rows.filter((r) => r.pkg.type === type);
458
+ if (section.length > 0) {
459
+ lines.push("", c.bold(`${title} (${section.length})`));
460
+ lines.push(...section.map((r) => renderRow(r.pkg, r.cells, widths, c, links)));
461
+ }
462
+ }
463
+ return lines;
464
+ };
465
+ const footer = (result, c) => {
466
+ const s = result.summary;
467
+ return [
468
+ `${s.total} packages`,
469
+ c.red(`${s.deprecated} deprecated`),
470
+ c.yellow(`${s.stale} stale`),
471
+ c.cyan(`${s.outdated} outdated`),
472
+ c.magenta(`${s.unknown} unknown`),
473
+ c.blue(`${s.blocked} blocked`),
474
+ c.green(`${s.ok} ok`)
475
+ ].join(" · ");
476
+ };
477
+ const formatTable = (result, opts) => {
478
+ const color = opts.color ?? picocolors.default.isColorSupported;
479
+ const c = picocolors.default.createColors(color);
480
+ const visible = opts.onlyProblems ? result.packages.filter((p) => !(p.flags.length === 1 && p.flags[0] === "ok")) : result.packages;
481
+ const lines = result.packages.length === 0 ? ["No dependencies found"] : renderTable(visible, result.project, opts.now, c, color);
482
+ lines.push("", footer(result, c));
483
+ if (result.skipped.length > 0) {
484
+ lines.push("", c.bold("Skipped:"));
485
+ const width = Math.max(...result.skipped.map((s) => s.name.length));
486
+ for (const s of result.skipped) lines.push(` ${s.name.padEnd(width)} ${s.specifier} — ${s.reason}`);
487
+ }
488
+ return `${lines.join("\n")}\n`;
489
+ };
490
+ //#endregion
320
491
  exports.check = check;
492
+ exports.formatJson = formatJson;
493
+ exports.formatTable = formatTable;
package/lib/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { basename, join, resolve } from "path";
2
2
  import { readFileSync } from "fs";
3
3
  import { executeCommand } from "@ladamczyk/qoq-utils";
4
+ import pc from "picocolors";
4
5
  //#region src/deps.ts
5
6
  const FIELDS = [
6
7
  ["prod", "dependencies"],
@@ -113,6 +114,12 @@ const parseOutdated = (stdout) => {
113
114
  return Object.fromEntries(Object.entries(parsed).map(([name, v]) => [name, toEntry(v)]));
114
115
  };
115
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
+ };
116
123
  const UNEXPECTED = { unknown: "unexpected npm view output" };
117
124
  const buildView = (parsed) => {
118
125
  const tags = parsed["dist-tags"];
@@ -125,7 +132,8 @@ const buildView = (parsed) => {
125
132
  latest,
126
133
  lastPublish,
127
134
  deprecated: typeof dep === "string" && dep !== "" ? dep : null,
128
- latestNode: nodeRange(parsed.engines)
135
+ latestNode: nodeRange(parsed.engines),
136
+ repository: repoUrl(parsed.repository)
129
137
  };
130
138
  };
131
139
  const parseView = (stdout) => {
@@ -179,6 +187,17 @@ function addMonths(date, months) {
179
187
  result.setUTCFullYear(year, month, Math.min(date.getUTCDate(), lastDay));
180
188
  return result;
181
189
  }
190
+ function relativeAge(iso, now) {
191
+ const date = new Date(iso);
192
+ if (date > now) return "0d ago";
193
+ const diffMs = now.getTime() - date.getTime();
194
+ const days = Math.floor(diffMs / 864e5);
195
+ if (days < 30) return `${days}d ago`;
196
+ let months = 0;
197
+ while (addMonths(date, months + 1) <= now) months++;
198
+ if (months < 12) return `${months}m ago`;
199
+ return `${Math.floor(months / 12)}y ago`;
200
+ }
182
201
  //#endregion
183
202
  //#region src/check.ts
184
203
  const ALL_TYPES = [
@@ -217,6 +236,7 @@ const lookup = async (entry, ctx) => {
217
236
  "dist-tags",
218
237
  "deprecated",
219
238
  "engines",
239
+ "repository",
220
240
  "--json",
221
241
  "--prefix",
222
242
  ctx.cwd
@@ -259,6 +279,7 @@ const buildRow = (entry, view, hit, ctx) => {
259
279
  majorBump: outdated && isMajorBump(current, latest),
260
280
  deprecated: viewed?.deprecated ?? null,
261
281
  latestNode: viewed?.latestNode ?? null,
282
+ repository: viewed?.repository ?? null,
262
283
  lastPublish: published ? published.toISOString() : null,
263
284
  stale: published !== null && published < addMonths(ctx.now, -ctx.staleAfterMonths),
264
285
  unknown: "unknown" in view ? view.unknown : null
@@ -316,4 +337,131 @@ const check = async (options = {}) => {
316
337
  };
317
338
  };
318
339
  //#endregion
319
- export { check };
340
+ //#region src/format.ts
341
+ const HEADER = [
342
+ "Package",
343
+ "Current",
344
+ "Wanted",
345
+ "Latest",
346
+ "Location",
347
+ "Depended by",
348
+ "Last publish",
349
+ "Links",
350
+ "Flags"
351
+ ];
352
+ const RIGHT_ALIGNED = /* @__PURE__ */ new Set([
353
+ 1,
354
+ 2,
355
+ 3
356
+ ]);
357
+ const SECTIONS = [
358
+ ["prod", "dependencies"],
359
+ ["dev", "devDependencies"],
360
+ ["optional", "optionalDependencies"]
361
+ ];
362
+ const MAX_MESSAGE = 60;
363
+ const formatJson = (result) => `${JSON.stringify(result, null, 2)}\n`;
364
+ const truncate = (text) => text.length > MAX_MESSAGE ? `${text.slice(0, 59)}…` : text;
365
+ const flagText = (pkg, flag) => {
366
+ if (flag === "deprecated") return `deprecated ${truncate(pkg.deprecated ?? "")}`;
367
+ if (flag === "blocked") return `latest needs node ${pkg.latestNode ?? ""}`;
368
+ if (flag === "unknown") return `unknown ${pkg.unknown ?? ""}`;
369
+ return flag;
370
+ };
371
+ const publishText = (pkg, now) => pkg.lastPublish === null ? "-" : `${pkg.lastPublish.slice(0, 10)} (${relativeAge(pkg.lastPublish, now)})`;
372
+ const linksText = (pkg) => pkg.repository === null ? "npm" : "npm / repo";
373
+ const toCells = (pkg, project, now) => [
374
+ pkg.name,
375
+ pkg.current ?? "-",
376
+ pkg.wanted ?? pkg.current ?? "-",
377
+ pkg.latest ?? "-",
378
+ `node_modules/${pkg.name}`,
379
+ project,
380
+ publishText(pkg, now),
381
+ linksText(pkg)
382
+ ];
383
+ const flagColors = (c) => ({
384
+ deprecated: c.red,
385
+ stale: c.yellow,
386
+ outdated: c.cyan,
387
+ unknown: c.magenta,
388
+ blocked: c.blue,
389
+ ok: c.green
390
+ });
391
+ const flagsCell = (pkg, c) => {
392
+ const colors = flagColors(c);
393
+ return pkg.flags.map((flag) => colors[flag](flagText(pkg, flag))).join(" ");
394
+ };
395
+ const hyperlink = (url, text) => `\u001b]8;;${url}\u001b\\${text}\u001b]8;;\u001b\\`;
396
+ const npmUrl = (pkg) => `https://www.npmjs.com/package/${pkg.queriedName ?? pkg.name}`;
397
+ const plain = (text) => text;
398
+ const nameColor = (pkg, c) => {
399
+ if (!pkg.outdated) return plain;
400
+ return pkg.current === pkg.wanted ? c.yellow : c.red;
401
+ };
402
+ const latestColor = (pkg, c) => {
403
+ if (pkg.majorBump) return c.red;
404
+ return pkg.outdated ? c.magenta : plain;
405
+ };
406
+ const linkPaint = (pkg) => (text) => text.split(" / ").map((label, i) => hyperlink(i === 0 ? npmUrl(pkg) : pkg.repository ?? "", label)).join(" / ");
407
+ const paints = (pkg, c, links) => [
408
+ nameColor(pkg, c),
409
+ plain,
410
+ pkg.outdated ? c.green : plain,
411
+ latestColor(pkg, c),
412
+ c.dim,
413
+ c.dim,
414
+ plain,
415
+ links ? linkPaint(pkg) : plain
416
+ ];
417
+ const renderCell = (text, width, column, paint) => {
418
+ const gap = " ".repeat(Math.max(0, width - text.length));
419
+ return RIGHT_ALIGNED.has(column) ? gap + paint(text) : paint(text) + gap;
420
+ };
421
+ const renderRow = (pkg, cells, widths, c, links) => {
422
+ return [...paints(pkg, c, links).map((paint, i) => renderCell(cells[i] ?? "", widths[i] ?? 0, i, paint)), flagsCell(pkg, c)].join(" ").trimEnd();
423
+ };
424
+ const renderTable = (packages, project, now, c, links) => {
425
+ const rows = packages.map((pkg) => ({
426
+ pkg,
427
+ cells: toCells(pkg, project, now)
428
+ }));
429
+ const widths = HEADER.map((h, i) => Math.max(h.length, ...rows.map((r) => r.cells[i]?.length ?? 0)));
430
+ const header = HEADER.map((h, i) => i === HEADER.length - 1 ? h : renderCell(h, widths[i] ?? 0, i, plain));
431
+ const lines = [c.underline(c.bold(header.join(" ")))];
432
+ for (const [type, title] of SECTIONS) {
433
+ const section = rows.filter((r) => r.pkg.type === type);
434
+ if (section.length > 0) {
435
+ lines.push("", c.bold(`${title} (${section.length})`));
436
+ lines.push(...section.map((r) => renderRow(r.pkg, r.cells, widths, c, links)));
437
+ }
438
+ }
439
+ return lines;
440
+ };
441
+ const footer = (result, c) => {
442
+ const s = result.summary;
443
+ return [
444
+ `${s.total} packages`,
445
+ c.red(`${s.deprecated} deprecated`),
446
+ c.yellow(`${s.stale} stale`),
447
+ c.cyan(`${s.outdated} outdated`),
448
+ c.magenta(`${s.unknown} unknown`),
449
+ c.blue(`${s.blocked} blocked`),
450
+ c.green(`${s.ok} ok`)
451
+ ].join(" · ");
452
+ };
453
+ const formatTable = (result, opts) => {
454
+ const color = opts.color ?? pc.isColorSupported;
455
+ const c = pc.createColors(color);
456
+ const visible = opts.onlyProblems ? result.packages.filter((p) => !(p.flags.length === 1 && p.flags[0] === "ok")) : result.packages;
457
+ const lines = result.packages.length === 0 ? ["No dependencies found"] : renderTable(visible, result.project, opts.now, c, color);
458
+ lines.push("", footer(result, c));
459
+ if (result.skipped.length > 0) {
460
+ lines.push("", c.bold("Skipped:"));
461
+ const width = Math.max(...result.skipped.map((s) => s.name.length));
462
+ for (const s of result.skipped) lines.push(` ${s.name.padEnd(width)} ${s.specifier} — ${s.reason}`);
463
+ }
464
+ return `${lines.join("\n")}\n`;
465
+ };
466
+ //#endregion
467
+ export { check, formatJson, formatTable };
@@ -1,2 +1,3 @@
1
1
  export { check } from './check.ts';
2
+ export { formatJson, formatTable } from './format.ts';
2
3
  export type { ICheckOptions, ICheckResult, IPackageResult, TRunner, ISkipped, ISummary, TDepType, TFlag, TProblem, } from './types.ts';
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.1",
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": {