deployhealth-scan 0.3.0 → 0.3.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 +10 -5
- package/dist/deployhealth-scan.mjs +20 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,8 +77,8 @@ Same-line, regex-based scanning misses some things on purpose:
|
|
|
77
77
|
## Try it locally
|
|
78
78
|
|
|
79
79
|
```sh
|
|
80
|
-
npx deployhealth-scan@0.3.
|
|
81
|
-
npx deployhealth-scan@0.3.
|
|
80
|
+
npx deployhealth-scan@0.3.1 --dry-run # grouped findings with file:line
|
|
81
|
+
npx deployhealth-scan@0.3.1 --dry-run --json # the same, as JSON
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
With `--dry-run` nothing leaves your machine.
|
|
@@ -113,14 +113,14 @@ jobs:
|
|
|
113
113
|
env:
|
|
114
114
|
DEPLOYHEALTH_TOKEN: ${{ secrets.DEPLOYHEALTH_TOKEN }}
|
|
115
115
|
run: |
|
|
116
|
-
npx --yes deployhealth-scan@0.3.
|
|
116
|
+
npx --yes deployhealth-scan@0.3.1 \
|
|
117
117
|
--url https://deployhealth.dev \
|
|
118
118
|
--token "$DEPLOYHEALTH_TOKEN" \
|
|
119
119
|
--sha "$GITHUB_SHA" \
|
|
120
120
|
--branch "$GITHUB_REF_NAME"
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Pin the version (`@0.3.
|
|
123
|
+
Pin the version (`@0.3.1`) so an update never runs in your CI unreviewed. For a self-hosted
|
|
124
124
|
deployhealth, change `--url` to your instance.
|
|
125
125
|
|
|
126
126
|
## What it sends
|
|
@@ -134,7 +134,8 @@ names they define; values are neither sent nor printed.
|
|
|
134
134
|
|
|
135
135
|
```
|
|
136
136
|
--url <url> deployhealth base URL (required unless --dry-run)
|
|
137
|
-
--token <token> the project's ingest token, dh_... (required unless --dry-run
|
|
137
|
+
--token <token> the project's ingest token, dh_... (required unless --dry-run;
|
|
138
|
+
default: the DEPLOYHEALTH_TOKEN environment variable)
|
|
138
139
|
--sha <sha> commit being deployed (default: git rev-parse HEAD)
|
|
139
140
|
--branch <name> branch being deployed (default: current git branch)
|
|
140
141
|
--dir <path> directory to scan (default: current directory)
|
|
@@ -151,6 +152,10 @@ names they define; values are neither sent nor printed.
|
|
|
151
152
|
-h, --help show this help
|
|
152
153
|
```
|
|
153
154
|
|
|
155
|
+
The token can come from the `DEPLOYHEALTH_TOKEN` environment variable instead of `--token`, which
|
|
156
|
+
keeps it out of process listings. A `--url` that is plain `http:` to anything but this machine
|
|
157
|
+
prints a warning: the token and the report would travel unencrypted.
|
|
158
|
+
|
|
154
159
|
Exit codes: `0` success, `1` the report failed (network or HTTP error), `2` usage error.
|
|
155
160
|
|
|
156
161
|
## License
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execFileSync as
|
|
3
|
-
|
|
4
|
-
`;case"r":return"\r";case"t":return" ";default:return t}})}function Ge(e){let n=/(^|\s)#/.exec(e);return n?e.slice(0,n.index):e}var Be=[".env.example",".env",".env.local",".env.development",".env.development.local",".env.production",".env.production.local",".env.test",".env.test.local"],te=[".env.sample",".env.template",".env.dist",".env.defaults","example.env","sample.env","env.example"],se=/^\.env\.[A-Za-z0-9_-]{1,64}\.(?:example|sample|template)$/;var H=[...Be,...te],He=new Set(H),Me=new Set([".env.example",...te]);function w(e){return He.has(e)||se.test(e)}function re(e){return Me.has(e)||se.test(e)}function je(e,n){let t=s=>{let r=H.indexOf(s);return r===-1?H.length:r};return t(e)-t(n)||(e<n?-1:e>n?1:0)}function L(e){return[...new Set(e)].sort(je)}var ie=/^[0-9a-f]{7,64}$/i;function le({references:e,envFiles:n,isIgnored:t,usedOutsideCode:s}){let r=[],i=new Set(n.flatMap(p=>[...p.entries,...p.commented??[]].map(c=>c.key))),o=new Set([...e.map(p=>p.name),...s??[]]);for(let p of n.length===0?[]:e)t(p.name)||p.hasDefault||i.has(p.name)||r.push({kind:"missing",var_name:p.name,file:p.file,line:p.line,env_file:null});for(let p of n)for(let{key:c,line:f}of oe(p.entries))t(c)||o.has(c)||r.push({kind:"unused",var_name:c,file:p.path,line:f,env_file:p.path});let a=n.find(p=>p.name===".env"),l=n.find(p=>p.name===".env.example");if(a&&l)for(let[p,c]of[[a,l],[l,a]]){let f=new Set(c.entries.map(g=>g.key));for(let{key:g,line:d}of oe(p.entries))t(g)||f.has(g)||r.push({kind:"mismatch",var_name:g,file:p.path,line:d,env_file:c.path})}return r}function oe(e){let n=new Set;return e.filter(t=>n.has(t.key)?!1:(n.add(t.key),!0))}var ae={missing:0,unused:1,mismatch:2};function ue({scope:e,references:n,envFiles:t,isIgnored:s}){return[...new Set(n.map(i=>i.name))].filter(i=>!s(i)).sort().map(i=>{let o={var_name:i,scope:e,defined_in:L(t.filter(a=>[...a.entries,...a.commented??[]].some(l=>l.key===i)).map(a=>a.name))};return n.every(a=>a.name!==i||a.hasDefault)&&(o.optional=!0),o})}function pe(e,n){return ae[e.kind]-ae[n.kind]||ce(e.var_name,n.var_name)||ce(e.file??"",n.file??"")||(e.line??0)-(n.line??0)}function fe(e){let n={missing:new Set,unused:new Set,mismatch:new Set};for(let t of e)n[t.kind].add(t.var_name);return{missing:n.missing.size,unused:n.unused.size,mismatch:n.mismatch.size}}function ce(e,n){return e<n?-1:e>n?1:0}function ze(e){let n="";for(let t of e)t==="*"?n+=".*":t==="?"?n+=".":n+=t.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp(`^${n}$`)}function M(e){let n=e.map(ze);return t=>n.some(s=>s.test(t))}import{extname as Qe}from"path";function Ve(e){let n=e.split(/\r?\n/),t=[],s=null,r=0,i=null;for(let o=0;o<n.length;o++){let a=n[o];if(!s){if(a.trim()==="")continue;s={line:o+1,indent:a.length-a.trimStart().length,code:""}}let l="";for(let c=0;c<a.length;){let f=a[c];if(i){f==="\\"?(l+=a.slice(c,c+2),c+=2):a.startsWith(i,c)?(l+=i,c+=i.length,i=null):(l+=f,c++);continue}if(f==="#")break;if(f==='"'||f==="'"){i=a.startsWith(f.repeat(3),c)?f.repeat(3):f,l+=i,c+=i.length;continue}"([{".includes(f)?r++:")]}".includes(f)&&(r=Math.max(0,r-1)),l+=f,c++}let p=!i&&l.trimEnd().endsWith("\\");p&&(l=l.trimEnd().slice(0,-1)),i?.length===1&&(i=null),s.code=s.code?`${s.code} ${l.trim()}`:l.trim(),r===0&&!i&&!p&&(s.code&&t.push(s),s=null)}return s?.code&&t.push(s),t}function U(e,n){let t=[],s=0,r=null,i=0;for(let o=0;o<e.length;o++){let a=e[o];if(r){a==="\\"?o++:e.startsWith(r,o)&&(o+=r.length-1,r=null);continue}if(a==='"'||a==="'")r=e.startsWith(a.repeat(3),o)?a.repeat(3):a;else if("([{".includes(a))s++;else if(")]}".includes(a))s--;else if(a===n&&s===0){if(n==="="&&("=!<>:".includes(e[o-1]??"")||e[o+1]==="="))continue;t.push(e.slice(i,o)),i=o+1}}return t.push(e.slice(i)),t.map(o=>o.trim())}function j(e,n){let t=0;for(let s=n;s<e.length;s++)if(e[s]==="(")t++;else if(e[s]===")"&&--t===0)return e.slice(n+1,s);return null}var We=/^class\s+([A-Za-z_]\w*)\s*(\(?)/;function Ze(e){let n=[];return e.forEach((t,s)=>{let r=We.exec(t.code);if(!r)return;let i=r[2]?j(t.code,r[0].length-1)??"":"",o=U(i,",").filter(l=>l&&!l.includes("=")).map(l=>l.replace(/\[.*$/s,"").split(".").pop().trim()),a=[];for(let l=s+1;l<e.length&&e[l].indent>t.indent;l++)a.push(l);n.push({name:r[1],bases:o,header:s,body:a})}),n}var Ke=/^([A-Za-z_]\w*)\s*:(?!=)\s*(.+)$/s,Ye=/^(?:[A-Za-z_]\w*\.)*Field\s*\(/,ge=/^[rRbBuU]?(['"])([A-Za-z_][A-Za-z0-9_]*)\1$/,qe=/^[A-Za-z_][A-Za-z0-9_]*$/;function de(e,n,t=new Set){if(!e.includes("BaseSettings")&&t.size===0)return[];let s=Ve(e),r=Ze(s),i=new Map,o=c=>c==="BaseSettings"||t.has(c)||i.has(c);for(let c=!0;c;){c=!1;for(let f of r)!i.has(f.name)&&f.bases.some(o)&&(i.set(f.name,f),c=!0)}if(i.size===0)return[];let a=new Set([...i.keys(),...r.filter(c=>c.bases.includes("BaseModel")).map(c=>c.name)]),l=(c,f=new Set)=>{for(let d of[c.header,...c.body]){let h=/\benv_prefix\s*=\s*[rRuU]?(['"])([A-Za-z0-9_]*)\1/.exec(s[d].code);if(h)return h[2]}f.add(c.name);let g=c.bases.map(d=>i.get(d)).find(d=>d&&!f.has(d.name));return g?l(g,f):""},p=[];for(let c of i.values()){if(c.body.length===0)continue;let f=s[c.body[0]].indent,g=l(c),d=c.body.some(h=>/\bcase_sensitive\s*=\s*True\b/.test(s[h].code));for(let h of c.body){let v=s[h];if(v.indent!==f)continue;let y=Ke.exec(v.code);if(!y)continue;let[,E,C]=y;if(E.startsWith("_")||E==="model_config")continue;let[N="",x]=U(C,"=");if(/^(?:typing\.)?ClassVar\b/.test(N)||Xe(N,a))continue;let{value:F,aliases:T}=Je(x),G=/\bNone\b|\bOptional\s*\[/.test(N),$=F!==null&&(F!=="None"||G),u=v.indent+1;if(T.length>0){let I=T.length>1;for(let k of T){let O={name:d?k:k.toUpperCase(),file:n,line:v.line,column:u,syntax:"BaseSettings"};($||I)&&(O.hasDefault=!0),p.push(O)}continue}let m=d?`${g}${E}`:`${g}${E}`.toUpperCase();if(!qe.test(m))continue;let _={name:m,file:n,line:v.line,column:u,syntax:"BaseSettings"};$&&(_.hasDefault=!0),p.push(_)}}return p}function Xe(e,n){let t=e.replace(/^(?:typing\.)?Optional\s*\[(.*)\]$/s,"$1").split("|").map(s=>s.trim()).filter(s=>s!=="None");return t.length===1&&n.has(t[0].replace(/^['"]|['"]$/g,""))}function Je(e){if(e===void 0||e==="")return{value:null,aliases:[]};let n=Ye.exec(e);if(!n)return{value:e,aliases:[]};let t=U(j(e,n[0].length-1)??"",",").filter(Boolean),s=null,r=[];for(let[i,o]of t.entries()){let a=/^([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/s.exec(o);if(!a){i===0&&o!=="..."&&o!=="Ellipsis"&&(s=o);continue}let[,l,p]=a;if(l==="default")s=p==="..."||p==="Ellipsis"?null:p;else if(l==="default_factory")s="factory";else if(l==="alias"||l==="validation_alias"||l==="env"){let c=ge.exec(p);c&&r.push(c[2]);let f=/^(?:[A-Za-z_]\w*\.)*AliasChoices\s*\(/.exec(p);if(f)for(let g of U(j(p,f[0].length-1)??"",",")){let d=ge.exec(g);d&&r.push(d[2])}}}return{value:s,aliases:[...new Set(r)]}}var he={".ts":"javascript",".tsx":"javascript",".mts":"javascript",".cts":"javascript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".cjs":"javascript",".py":"python",".go":"go",".rb":"ruby"},ye=new Set(Object.keys(he)),V="(?<name>[A-Za-z_][A-Za-z0-9_]*)",R=e=>`(?<q>[${e}])${V}\\k<q>`,P=/^\s*(?:\?\?|\|\|)=?\s*(?!(?:undefined|null)\b)\S/,me=/^\s*(?:,|\)\s*or\b)\s*(?!None\b)[^\s)]/,en=/^\s*(?:,\s*(?!nil\b)\S|\)\s*(?:\{|do\b))/,nn=/^\s*\|\|\s*(?!nil\b)\S/,tn={javascript:[{syntax:"process.env",regex:new RegExp(`\\bprocess\\.env(?:\\?\\.|\\.)${V}\\b`,"g"),defaultAfter:P},{syntax:"process.env",regex:new RegExp(`\\bprocess\\.env(?:\\?\\.)?\\[\\s*${R("'\"`")}\\s*\\]`,"g"),defaultAfter:P},{syntax:"import.meta.env",regex:new RegExp(`\\bimport\\.meta\\.env(?:\\?\\.|\\.)${V}\\b`,"g"),defaultAfter:P},{syntax:"import.meta.env",regex:new RegExp(`\\bimport\\.meta\\.env(?:\\?\\.)?\\[\\s*${R("'\"`")}\\s*\\]`,"g"),defaultAfter:P}],python:[{syntax:"os.environ",regex:new RegExp(`\\bos\\.environ\\s*\\[\\s*${R(`'"`)}\\s*\\]`,"g")},{syntax:"os.environ",regex:new RegExp(`\\bos\\.environ\\.get\\s*\\(\\s*${R(`'"`)}`,"g"),defaultAfter:me},{syntax:"os.getenv",regex:new RegExp(`\\bos\\.getenv\\s*\\(\\s*${R(`'"`)}`,"g"),defaultAfter:me}],go:[{syntax:"os.Getenv",regex:new RegExp(`\\bos\\.Getenv\\s*\\(\\s*${R('"`')}\\s*\\)`,"g")},{syntax:"os.LookupEnv",regex:new RegExp(`\\bos\\.LookupEnv\\s*\\(\\s*${R('"`')}\\s*\\)`,"g")}],ruby:[{syntax:"ENV",regex:new RegExp(`\\bENV\\s*\\[\\s*${R(`'"`)}\\s*\\]`,"g"),defaultAfter:nn},{syntax:"ENV",regex:new RegExp(`\\bENV\\.fetch(?:\\s*\\(\\s*|\\s+)${R(`'"`)}`,"g"),defaultAfter:en}]};function W(e){return he[Qe(e).toLowerCase()]}function Z(e,n,t){let s=[],r=e.split(/\r?\n/);for(let i=0;i<r.length;i++){let o=r[i];for(let{syntax:a,regex:l,defaultAfter:p}of tn[n]){l.lastIndex=0;for(let c=l.exec(o);c;c=l.exec(o)){let f=c.groups?.name;if(!f||cn(o,c.index,c.index+c[0].length))continue;let g={name:f,file:t,line:i+1,column:c.index+1,syntax:a};p?.test(o.slice(c.index+c[0].length))&&(g.hasDefault=!0),s.push(g)}}n==="javascript"&&s.push(...on(o,i+1,t))}return n==="python"&&s.push(...de(e,t)),ln(s).sort((i,o)=>i.line-o.line||i.column-o.column)}var z=/\{([^{}]*)\}\s*(?::[^=;{}]+)?=\s*(process\.env|import\.meta\.env)\b(?!\s*\.|\s*\[|\s*\?\.)/g,sn=/^\s*(?:(['"])([A-Za-z_][A-Za-z0-9_]*)\1|([A-Za-z_][A-Za-z0-9_]*))\s*(?::\s*[A-Za-z_$][\w$]*\s*)?(=\s*(.*))?$/s,rn=/^(?!(?:undefined|null)\b)\S/;function on(e,n,t){let s=[];z.lastIndex=0;for(let r=z.exec(e);r;r=z.exec(e)){let i=r[2]==="process.env"?"process.env":"import.meta.env",o=r.index+1;for(let a of r[1].split(",")){let l=sn.exec(a),p=l?.[2]??l?.[3];if(l&&p){let c={name:p,file:t,line:n,column:o+a.indexOf(p)+1,syntax:i};l[4]&&rn.test(l[5].trim())&&(c.hasDefault=!0),s.push(c)}o+=a.length+1}}return s}var an=new Set(['"',"'","`"]);function cn(e,n,t){let s=e[n-1];return s!==void 0&&an.has(s)&&e[t]===s}function ln(e){let n=new Map;for(let t of[...e].sort((s,r)=>s.line-r.line||s.column-r.column)){let s=`${t.line}\0${t.name}`,r=n.get(s);r?t.hasDefault||delete r.hasDefault:n.set(s,{...t})}return[...n.values()]}var A=class e{constructor(n){this.rules=n}rules;static empty(){return new e([])}extend(n,t){let s=un(t).map(r=>({...r,base:n}));return s.length===0?this:new e([...this.rules,...s])}ignores(n,t){let s=!1;for(let r of this.rules){if(r.dirOnly&&!t)continue;let i=dn(r.base,n);i!==null&&r.regex.test(i)&&(s=!r.negate)}return s}};function un(e){let n=[];for(let t of e.split(/\r?\n/)){let s=pn(t);s&&n.push(s)}return n}function pn(e){let n=fn(e);if(n===""||n.startsWith("#"))return null;let t=!1;n.startsWith("!")?(t=!0,n=n.slice(1)):(n.startsWith("\\!")||n.startsWith("\\#"))&&(n=n.slice(1));let s=!1;if(n.endsWith("/")&&(s=!0,n=n.replace(/\/+$/,"")),n==="")return null;let r=n.includes("/");return n=n.replace(/^\/+/,""),!r&&!n.startsWith("**/")&&(n=`**/${n}`),{regex:new RegExp(`^${gn(n)}$`),negate:t,dirOnly:s,source:e}}function fn(e){let n=e.length;for(;n>0&&e[n-1]===" "&&e[n-2]!=="\\";)n--;return e.slice(0,n)}function gn(e){let n="",t=0;for(;t<e.length;){let s=e[t];if(s==="*"){if(e[t+1]==="*"){let r=t===0,i=r||e[t-1]==="/",o=e[t+2]==="/",a=t+2===e.length;if(i&&o){n+="(?:.*/)?",t+=3;continue}if(i&&a&&!r){n+=".*",t+=2;continue}n+="[^/]*",t+=2;continue}n+="[^/]*",t++;continue}if(s==="?"){n+="[^/]",t++;continue}if(s==="["){let r=e.indexOf("]",t+2);if(r!==-1){let i=e.slice(t+1,r);i.startsWith("!")&&(i=`^${i.slice(1)}`),n+=`[${i.replace(/\\/g,"\\\\")}]`,t=r+1;continue}}if(s==="\\"&&t+1<e.length){n+=Ee(e[t+1]),t+=2;continue}n+=Ee(s),t++}return n}function Ee(e){return e.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&")}function dn(e,n){return e===""?n:n.startsWith(`${e}/`)?n.slice(e.length+1):null}var _e=new Set(["test","tests","__tests__","spec","e2e","fixtures","__fixtures__","testdata","playwright","cypress","mocks","__mocks__","testing"]),mn=[/.\.test\./,/.\.spec\./,/.\.e2e\./,/.\.e2e-spec\./,/.\.cy\./,/_test\.go$/,/^test_.*\.py$/,/_test\.py$/,/^conftest\.py$/,/_spec\.rb$/,/^(?:playwright|vitest|jest|cypress)\.config\./,/^vitest\.workspace\./,/^(?:vitest|jest)\.setup\./];function ve(e){return!w(e)&&mn.some(n=>n.test(e))}var K=new Set(["node_modules","dist",".next","venv",".venv",".yarn","vendor","third_party","bower_components","out","coverage",".turbo",".vercel",".output",".svelte-kit",".nuxt",".cache",".pnpm-store","__pycache__","site-packages"]);function Y(e){return e===".pnp.cjs"||e===".pnp.loader.mjs"||/\.min\.(?:js|mjs|cjs)$/.test(e)}var q=512*1024;import{readdir as Se,readFile as hn}from"fs/promises";import{join as X}from"path";var Re=new Set([".git",...K]);async function xe(e,n){let t=n.skipDirs??Re,s=n.respectGitignore??!0,r=[],i=[],o=[],a=[],l=A.empty().extend("",(n.exclude??[]).join(`
|
|
5
|
-
`));async function p(f,g,d){let h=f===""?e:X(e,f),v;try{v=await Se(h,{withFileTypes:!0})}catch(y){i.push({file:f||".",message:`cannot read directory: ${y.message}`});return}s&&v.some(y=>y.name===".gitignore"&&y.isFile())&&(g=g.extend(f,await hn(X(h,".gitignore"),"utf8")));for(let y of v){if(y.isSymbolicLink())continue;let E=f===""?y.name:`${f}/${y.name}`;if(y.isDirectory()){if(l.ignores(E,!0)||g.ignores(E,!0))continue;if(t.has(y.name)){K.has(y.name)&&!d&&await c(E)&&a.push(`${E}/`);continue}await p(E,g,d||n.skipTests===!0&&_e.has(y.name))}else if(y.isFile()){if(!n.include(E,y.name)||l.ignores(E,!1)||g.ignores(E,!1)&&!n.keepIgnored?.(y.name))continue;Y(y.name)?d||a.push(E):n.skipTests&&(d||ve(y.name))?o.push(E):r.push(E)}}}async function c(f){let g;try{g=await Se(X(e,f),{withFileTypes:!0})}catch{return!1}for(let d of g){let h=`${f}/${d.name}`;if(d.isFile()&&n.include(h,d.name))return!0}for(let d of g)if(d.isDirectory()&&!d.isSymbolicLink()&&await c(`${f}/${d.name}`))return!0;return!1}return await p("",A.empty(),!1),r.sort(),o.sort(),a.sort(),{files:r,warnings:i,testFiles:o,vendored:a}}async function Ie(e,n={}){let{files:t,warnings:s,testFiles:r,vendored:i}=await xe(e,{include:_n,exclude:n.exclude,keepIgnored:w,skipTests:!n.includeTests});return vn(t,{read:a=>yn(be(e,a),"utf8"),size:async a=>(await En(be(e,a))).size},s,n,r,i)}function _n(e,n){return w(n)||B(n)||ye.has(J.extname(e).toLowerCase())}async function vn(e,{read:n,size:t},s,r,i,o){let a=[],l=[],p=[],c=[];for(let u of e){let m=J.basename(u);if(!w(m)){await t(u)>q?c.push(u):(B(m)?p:l).push(u);continue}let{entries:_,commented:I,invalid:k}=ne(await n(u));for(let{line:O}of k)s.push({file:u,line:O,message:"ignored a line that is not KEY=value"});a.push({path:u,name:m,entries:_,commented:re(m)?I:[]})}let f=new Set(a.map(u=>b(u.path))),g=new Map;for(let u of l){let m=W(u);if(!m)continue;let _=Z(await n(u),m,u);if(_.length===0)continue;let I=Te(b(u),f);g.set(I,[...g.get(I)??[],..._])}let d=new Map,h=(u,m)=>{let _=Te(b(u),f);d.set(_,new Set([...d.get(_)??[],...m]))};for(let u of i){let m=W(u);!m||await t(u)>q||h(u,Z(await n(u),m,u).map(_=>_.name))}for(let u of p)h(u,Q(await n(u)));let v=M(r.ignore??[]),y=M(r.defaultIgnore===!1?[]:ee),E=u=>v(u)||y(u),C=[...g.values()].flat().map(u=>u.name).concat(a.flatMap(u=>u.entries.map(m=>m.key))),N=[...new Set(C.filter(u=>y(u)&&!v(u)))].sort(),x=[...new Set([...f,...g.keys()])].sort(),F=x.map(u=>{let m=L(a.filter(_=>b(_.path)===u).map(_=>_.name));return m.length>64&&s.push({file:u||".",message:`${m.length} env files; only the first ${64} are reported`}),{scope:u,env_files:m.slice(0,64)}}),T=x.flatMap(u=>le({references:g.get(u)??[],envFiles:a.filter(m=>b(m.path)===u),isIgnored:E,usedOutsideCode:d.get(u)})).sort(pe),G=x.flatMap(u=>ue({scope:u,references:g.get(u)??[],envFiles:a.filter(m=>b(m.path)===u),isIgnored:E}).map(m=>({...m,defined_in:m.defined_in.slice(0,64)}))),$=x.flatMap(u=>(g.get(u)??[]).filter(m=>!E(m.name)).map(m=>({...m,scope:u}))).sort((u,m)=>u.file.localeCompare(m.file)||u.line-m.line||u.column-m.column);return{findings:T,references:$,variables:G,counts:fe(T),scopes:x,envScopes:F,defaultIgnored:N,testFilesSkipped:i.length,vendoredSkipped:o,tooLargeSkipped:c,sourceFiles:l.length,envFiles:a.map(u=>u.path),warnings:s}}function b(e){let n=J.dirname(e);return n==="."?"":n}function Te(e,n){for(let t=e;t!=="";t=b(t))if(n.has(t))return t;return""}var we="0.3.0";var S={ok:0,failed:1,usage:2},Ae=`Usage: deployhealth-scan [options]
|
|
2
|
+
import{execFileSync as Un}from"child_process";import{resolve as Hn}from"path";import{parseArgs as Gn}from"util";import{readFile as Ln,stat as $n}from"fs/promises";import{join as Le,posix as ne}from"path";function j(e){return/^(?:docker-)?compose(?:[.-][A-Za-z0-9_.-]+)?\.ya?ml$/.test(e)}var je=/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)(?=[}:?+-])|([A-Za-z_][A-Za-z0-9_]*))/g;function ie(e){let n=new Set;for(let t of e.split(/\r?\n/)){if(t.trimStart().startsWith("#"))continue;let s=t.replace(/\$\$/g,"");for(let r of s.matchAll(je))n.add(r[1]??r[2])}return n}var oe=["NODE_ENV","NEXT_RUNTIME","npm_*","HOME","PATH","PWD","USER","SHELL","TMPDIR","HOSTNAME","CI","GITHUB_ACTION","GITHUB_ACTION_PATH","GITHUB_ACTION_REPOSITORY","GITHUB_ACTIONS","GITHUB_ACTOR","GITHUB_ACTOR_ID","GITHUB_API_URL","GITHUB_ARTIFACTS","GITHUB_ARTIFACTS_LIST","GITHUB_BASE_REF","GITHUB_ENV","GITHUB_EVENT_NAME","GITHUB_EVENT_PATH","GITHUB_GRAPHQL_URL","GITHUB_HEAD_REF","GITHUB_JOB","GITHUB_OUTPUT","GITHUB_PATH","GITHUB_REF","GITHUB_REF_NAME","GITHUB_REF_PROTECTED","GITHUB_REF_TYPE","GITHUB_REPOSITORY","GITHUB_REPOSITORY_ID","GITHUB_REPOSITORY_OWNER","GITHUB_REPOSITORY_OWNER_ID","GITHUB_RETENTION_DAYS","GITHUB_RUN_ATTEMPT","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SERVER_URL","GITHUB_SHA","GITHUB_STEP_SUMMARY","GITHUB_TRIGGERING_ACTOR","GITHUB_WORKFLOW","GITHUB_WORKFLOW_REF","GITHUB_WORKFLOW_SHA","GITHUB_WORKSPACE","RUNNER_*","VERCEL","VERCEL_*","NEXT_PUBLIC_VERCEL_*","RAILWAY_*","RENDER","RENDER_*","FLY_*"];import{readFile as Yn}from"fs/promises";var We=/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=(.*)$/,ze=/^\s*#+\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/,Ve=new Set(['"',"'","`"]),ae="-----BEGIN ",Ke="-----END ";function le(e){let n=e.replace(/^\uFEFF/,"").split(/\r?\n/),t=[],s=[],r=[],a=null,o=i=>{for(let l=i+1;l<n.length;l++)if((n[l]??"").trim().startsWith(Ke))return l;return-1};for(let i=0;i<n.length;i++){let l=n[i]??"",c=l.trim();if(c.startsWith("#")){let m=ze.exec(l)?.[1];m&&s.push({key:m,line:i+1});continue}if(c==="")continue;if(c.startsWith(ae)){let m=o(i);if(m===-1){a={line:i+1,kind:"block"};break}i=m;continue}let u=We.exec(l);if(!u){r.push({line:i+1,text:l});continue}let p=u[1],d=(u[2]??"").trimStart(),E=i+1,T=d[0];if(T!==void 0&&Ve.has(T)){let m=Ze(n,i,d,T);if(t.push({key:p,value:m.value,line:E}),m.endIndex===-1){a={line:E,kind:"quote"};break}i=m.endIndex;continue}let _=qe(d).trim();if(t.push({key:p,value:_,line:E}),_.startsWith(ae)){let m=o(i);if(m===-1){a={line:E,kind:"block"};break}i=m}}return{entries:t,commented:s,invalid:r,unterminated:a}}function Ze(e,n,t,s){let r=[],a=t.slice(1);for(let o=n;;){let i=Ye(a,s);if(i!==-1){r.push(a.slice(0,i));let l=r.join(`
|
|
3
|
+
`);return{value:s==='"'?ce(l):l,endIndex:o}}if(r.push(a),o++,o>=e.length){let l=r.join(`
|
|
4
|
+
`);return{value:s==='"'?ce(l):l,endIndex:-1}}a=e[o]??""}}function Ye(e,n){for(let t=0;t<e.length;t++){let s=e[t];if(s==="\\"){t++;continue}if(s===n)return t}return-1}function ce(e){return e.replace(/\\([nrt"\\])/g,(n,t)=>{switch(t){case"n":return`
|
|
5
|
+
`;case"r":return"\r";case"t":return" ";default:return t}})}function qe(e){let n=/(^|\s)#/.exec(e);return n?e.slice(0,n.index):e}var Xe=[".env.example",".env",".env.local",".env.development",".env.development.local",".env.production",".env.production.local",".env.test",".env.test.local"],ue=[".env.sample",".env.template",".env.dist",".env.defaults","example.env","sample.env","env.example"],fe=/^\.env\.[A-Za-z0-9_-]{1,64}\.(?:example|sample|template)$/;var W=[...Xe,...ue],Je=new Set(W),Qe=new Set([".env.example",...ue]);function $(e){return Je.has(e)||fe.test(e)}function pe(e){return Qe.has(e)||fe.test(e)}function en(e,n){let t=s=>{let r=W.indexOf(s);return r===-1?W.length:r};return t(e)-t(n)||(e<n?-1:e>n?1:0)}function H(e){return[...new Set(e)].sort(en)}var de=/^[0-9a-f]{7,64}$/i;function Ee({references:e,envFiles:n,isIgnored:t,usedOutsideCode:s}){let r=[],a=new Set(n.flatMap(c=>[...c.entries,...c.commented??[]].map(u=>u.key))),o=new Set([...e.map(c=>c.name),...s??[]]);for(let c of n.length===0?[]:e)t(c.name)||c.hasDefault||a.has(c.name)||r.push({kind:"missing",var_name:c.name,file:c.file,line:c.line,env_file:null});for(let c of n)for(let{key:u,line:p}of ge(c.entries))t(u)||o.has(u)||r.push({kind:"unused",var_name:u,file:c.path,line:p,env_file:c.path});let i=n.find(c=>c.name===".env"),l=n.find(c=>c.name===".env.example");if(i&&l)for(let[c,u]of[[i,l],[l,i]]){let p=new Set(u.entries.map(d=>d.key));for(let{key:d,line:E}of ge(c.entries))t(d)||p.has(d)||r.push({kind:"mismatch",var_name:d,file:c.path,line:E,env_file:u.path})}return r}function ge(e){let n=new Set;return e.filter(t=>n.has(t.key)?!1:(n.add(t.key),!0))}var me={missing:0,unused:1,mismatch:2};function _e({scope:e,references:n,envFiles:t,isIgnored:s}){let r=new Map;for(let i of n)r.set(i.name,(r.get(i.name)??!0)&&i.hasDefault===!0);let a=new Map;for(let i of t)for(let l of new Set([...i.entries,...i.commented??[]].map(c=>c.key))){let c=a.get(l);c?c.push(i.name):a.set(l,[i.name])}return[...r.keys()].filter(i=>!s(i)).sort().map(i=>{let l={var_name:i,scope:e,defined_in:H(a.get(i)??[])};return r.get(i)&&(l.optional=!0),l})}function ye(e,n){return me[e.kind]-me[n.kind]||he(e.var_name,n.var_name)||he(e.file??"",n.file??"")||(e.line??0)-(n.line??0)}function Se(e){let n={missing:new Set,unused:new Set,mismatch:new Set};for(let t of e)n[t.kind].add(t.var_name);return{missing:n.missing.size,unused:n.unused.size,mismatch:n.mismatch.size}}function he(e,n){return e<n?-1:e>n?1:0}function nn(e){let n="";for(let t of e)t==="*"?n+=".*":t==="?"?n+=".":n+=t.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp(`^${n}$`)}function z(e){let n=e.map(nn);return t=>n.some(s=>s.test(t))}import{extname as fn}from"path";function tn(e){let n=e.split(/\r?\n/),t=[],s=null,r=0,a=null;for(let o=0;o<n.length;o++){let i=n[o];if(!s){if(i.trim()==="")continue;s={line:o+1,indent:i.length-i.trimStart().length,code:""}}let l="";for(let u=0;u<i.length;){let p=i[u];if(a){p==="\\"?(l+=i.slice(u,u+2),u+=2):i.startsWith(a,u)?(l+=a,u+=a.length,a=null):(l+=p,u++);continue}if(p==="#")break;if(p==='"'||p==="'"){a=i.startsWith(p.repeat(3),u)?p.repeat(3):p,l+=a,u+=a.length;continue}"([{".includes(p)?r++:")]}".includes(p)&&(r=Math.max(0,r-1)),l+=p,u++}let c=!a&&l.trimEnd().endsWith("\\");c&&(l=l.trimEnd().slice(0,-1)),a?.length===1&&(a=null),s.code=s.code?`${s.code} ${l.trim()}`:l.trim(),r===0&&!a&&!c&&(s.code&&t.push(s),s=null)}return s?.code&&t.push(s),t}function G(e,n){let t=[],s=0,r=null,a=0;for(let o=0;o<e.length;o++){let i=e[o];if(r){i==="\\"?o++:e.startsWith(r,o)&&(o+=r.length-1,r=null);continue}if(i==='"'||i==="'")r=e.startsWith(i.repeat(3),o)?i.repeat(3):i;else if("([{".includes(i))s++;else if(")]}".includes(i))s--;else if(i===n&&s===0){if(n==="="&&("=!<>:".includes(e[o-1]??"")||e[o+1]==="="))continue;t.push(e.slice(a,o)),a=o+1}}return t.push(e.slice(a)),t.map(o=>o.trim())}function V(e,n){let t=0;for(let s=n;s<e.length;s++)if(e[s]==="(")t++;else if(e[s]===")"&&--t===0)return e.slice(n+1,s);return null}var sn=/^class\s+([A-Za-z_]\w*)\s*(\(?)/;function rn(e){let n=[],t=[];for(let s=0;s<e.length;s++){let r=e[s];for(;t.length>0&&r.indent<=t[t.length-1].indent;)t.pop().cls.end=s;let a=t[t.length-1];a&&(a.bodyIndent??=r.indent,r.indent===a.bodyIndent&&a.cls.direct.push(s));let o=sn.exec(r.code);if(!o)continue;let i=o[2]?V(r.code,o[0].length-1)??"":"",l=G(i,",").filter(u=>u&&!u.includes("=")).map(u=>u.replace(/\[.*$/s,"").split(".").pop().trim()),c={name:o[1],bases:l,header:s,end:e.length,direct:[]};n.push(c),t.push({cls:c,indent:r.indent,bodyIndent:null})}return n}var on=/^([A-Za-z_]\w*)\s*:(?!=)\s*(.+)$/s,an=/^(?:[A-Za-z_]\w*\.)*Field\s*\(/,ve=/^[rRbBuU]?(['"])([A-Za-z_][A-Za-z0-9_]*)\1$/,cn=/^[A-Za-z_][A-Za-z0-9_]*$/;function xe(e,n,t=new Set){if(!e.includes("BaseSettings")&&t.size===0)return[];let s=tn(e),r=rn(s),a=new Map;for(let h of r)for(let y of h.bases){let x=a.get(y);x?x.push(h):a.set(y,[h])}let o=new Set,i=["BaseSettings",...t],l=new Set(i);for(let h=0;h<i.length;h++)for(let y of a.get(i[h])??[])o.add(y),l.has(y.name)||(l.add(y.name),i.push(y.name));let c=new Map;for(let h of r)o.has(h)&&!c.has(h.name)&&c.set(h.name,h);if(c.size===0)return[];let u=new Set([...c.keys(),...r.filter(h=>h.bases.includes("BaseModel")).map(h=>h.name)]),p=h=>{let y=new Int32Array(s.length+1).fill(s.length);for(let x=s.length-1;x>=0;x--)y[x]=h.test(s[x].code)?x:y[x+1];return y},d=p(/\benv_prefix\s*=\s*[rRuU]?(['"])([A-Za-z0-9_]*)\1/),E=p(/\bcase_sensitive\s*=\s*True\b/),T=h=>{let y=d[h.header];return y<h.end?/\benv_prefix\s*=\s*[rRuU]?(['"])([A-Za-z0-9_]*)\1/.exec(s[y].code)[2]:null},_=new Map,m=h=>{let y=[],x=new Set,b="";for(let S=h;S;){let L=_.get(S);if(L!==void 0){b=L;break}y.push(S),x.add(S);let I=T(S);if(I!==null){b=I;break}S=S.bases.map(F=>c.get(F)).find(F=>F&&!x.has(F))}for(let S of y)_.set(S,b);return b},v=[];for(let h of c.values()){if(h.direct.length===0)continue;let y=m(h),x=E[h.header+1]<h.end;for(let b of h.direct){let S=s[b],L=on.exec(S.code);if(!L)continue;let[,I,F]=L;if(I.startsWith("_")||I==="model_config")continue;let[C="",f]=G(F,"=");if(/^(?:typing\.)?ClassVar\b/.test(C)||ln(C,u))continue;let{value:g,aliases:R}=un(f),N=/\bNone\b|\bOptional\s*\[/.test(C),k=g!==null&&(g!=="None"||N),O=S.indent+1;if(R.length>0){let Me=R.length>1;for(let se of R){let re={name:x?se:se.toUpperCase(),file:n,line:S.line,column:O,syntax:"BaseSettings"};(k||Me)&&(re.hasDefault=!0),v.push(re)}continue}let U=x?`${y}${I}`:`${y}${I}`.toUpperCase();if(!cn.test(U))continue;let te={name:U,file:n,line:S.line,column:O,syntax:"BaseSettings"};k&&(te.hasDefault=!0),v.push(te)}}return v}function ln(e,n){let t=e.replace(/^(?:typing\.)?Optional\s*\[(.*)\]$/s,"$1").split("|").map(s=>s.trim()).filter(s=>s!=="None");return t.length===1&&n.has(t[0].replace(/^['"]|['"]$/g,""))}function un(e){if(e===void 0||e==="")return{value:null,aliases:[]};let n=an.exec(e);if(!n)return{value:e,aliases:[]};let t=G(V(e,n[0].length-1)??"",",").filter(Boolean),s=null,r=[];for(let[a,o]of t.entries()){let i=/^([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/s.exec(o);if(!i){a===0&&o!=="..."&&o!=="Ellipsis"&&(s=o);continue}let[,l,c]=i;if(l==="default")s=c==="..."||c==="Ellipsis"?null:c;else if(l==="default_factory")s="factory";else if(l==="alias"||l==="validation_alias"||l==="env"){let u=ve.exec(c);u&&r.push(u[2]);let p=/^(?:[A-Za-z_]\w*\.)*AliasChoices\s*\(/.exec(c);if(p)for(let d of G(V(c,p[0].length-1)??"",",")){let E=ve.exec(d);E&&r.push(E[2])}}}return{value:s,aliases:[...new Set(r)]}}var Te={".ts":"javascript",".tsx":"javascript",".mts":"javascript",".cts":"javascript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".cjs":"javascript",".py":"python",".go":"go",".rb":"ruby"},be=new Set(Object.keys(Te)),Z="(?<name>[A-Za-z_][A-Za-z0-9_]*)",w=e=>`(?<q>[${e}])${Z}\\k<q>`,B=/^\s*(?:\?\?|\|\|)=?\s*(?!(?:undefined|null)\b)\S/,Re=/^\s*(?:,|\)\s*or\b)\s*(?!None\b)[^\s)]/,pn=/^\s*(?:,\s*(?!nil\b)\S|\)\s*(?:\{|do\b))/,dn=/^\s*\|\|\s*(?!nil\b)\S/,gn={javascript:[{syntax:"process.env",regex:new RegExp(`\\bprocess\\.env(?:\\?\\.|\\.)${Z}\\b`,"g"),defaultAfter:B},{syntax:"process.env",regex:new RegExp(`\\bprocess\\.env(?:\\?\\.)?\\[\\s*${w("'\"`")}\\s*\\]`,"g"),defaultAfter:B},{syntax:"import.meta.env",regex:new RegExp(`\\bimport\\.meta\\.env(?:\\?\\.|\\.)${Z}\\b`,"g"),defaultAfter:B},{syntax:"import.meta.env",regex:new RegExp(`\\bimport\\.meta\\.env(?:\\?\\.)?\\[\\s*${w("'\"`")}\\s*\\]`,"g"),defaultAfter:B}],python:[{syntax:"os.environ",regex:new RegExp(`\\bos\\.environ\\s*\\[\\s*${w(`'"`)}\\s*\\]`,"g")},{syntax:"os.environ",regex:new RegExp(`\\bos\\.environ\\.get\\s*\\(\\s*${w(`'"`)}`,"g"),defaultAfter:Re},{syntax:"os.getenv",regex:new RegExp(`\\bos\\.getenv\\s*\\(\\s*${w(`'"`)}`,"g"),defaultAfter:Re}],go:[{syntax:"os.Getenv",regex:new RegExp(`\\bos\\.Getenv\\s*\\(\\s*${w('"`')}\\s*\\)`,"g")},{syntax:"os.LookupEnv",regex:new RegExp(`\\bos\\.LookupEnv\\s*\\(\\s*${w('"`')}\\s*\\)`,"g")}],ruby:[{syntax:"ENV",regex:new RegExp(`\\bENV\\s*\\[\\s*${w(`'"`)}\\s*\\]`,"g"),defaultAfter:dn},{syntax:"ENV",regex:new RegExp(`\\bENV\\.fetch(?:\\s*\\(\\s*|\\s+)${w(`'"`)}`,"g"),defaultAfter:pn}]};function Y(e){return Te[fn(e).toLowerCase()]}function q(e,n,t){let s=[],r=e.split(/\r?\n/);for(let a=0;a<r.length;a++){let o=r[a];for(let{syntax:i,regex:l,defaultAfter:c}of gn[n]){l.lastIndex=0;for(let u=l.exec(o);u;u=l.exec(o)){let p=u.groups?.name;if(!p||yn(o,u.index,u.index+u[0].length))continue;let d={name:p,file:t,line:a+1,column:u.index+1,syntax:i};c?.test(o.slice(u.index+u[0].length))&&(d.hasDefault=!0),s.push(d)}}if(n==="javascript")for(let i of En(o,a+1,t))s.push(i)}if(n==="python")for(let a of xe(e,t))s.push(a);return Sn(s).sort((a,o)=>a.line-o.line||a.column-o.column)}var K=/\{([^{}]*)\}\s*(?::[^=;{}]+)?=\s*(process\.env|import\.meta\.env)\b(?!\s*\.|\s*\[|\s*\?\.)/g,mn=/^\s*(?:(['"])([A-Za-z_][A-Za-z0-9_]*)\1|([A-Za-z_][A-Za-z0-9_]*))\s*(?::\s*[A-Za-z_$][\w$]*\s*)?(=\s*(.*))?$/s,hn=/^(?!(?:undefined|null)\b)\S/;function En(e,n,t){let s=[];K.lastIndex=0;for(let r=K.exec(e);r;r=K.exec(e)){let a=r[2]==="process.env"?"process.env":"import.meta.env",o=r.index+1;for(let i of r[1].split(",")){let l=mn.exec(i),c=l?.[2]??l?.[3];if(l&&c){let u={name:c,file:t,line:n,column:o+i.indexOf(c)+1,syntax:a};l[4]&&hn.test(l[5].trim())&&(u.hasDefault=!0),s.push(u)}o+=i.length+1}}return s}var _n=new Set(['"',"'","`"]);function yn(e,n,t){let s=e[n-1];return s!==void 0&&_n.has(s)&&e[t]===s}function Sn(e){let n=new Map;for(let t of e.slice().sort((s,r)=>s.line-r.line||s.column-r.column)){let s=`${t.line}\0${t.name}`,r=n.get(s);r?t.hasDefault||delete r.hasDefault:n.set(s,{...t})}return[...n.values()]}var Ie=new Set(["test","tests","__tests__","spec","e2e","fixtures","__fixtures__","testdata","playwright","cypress","mocks","__mocks__","testing"]),vn=[/.\.test\./,/.\.spec\./,/.\.e2e\./,/.\.e2e-spec\./,/.\.cy\./,/_test\.go$/,/^test_.*\.py$/,/_test\.py$/,/^conftest\.py$/,/_spec\.rb$/,/^(?:playwright|vitest|jest|cypress)\.config\./,/^vitest\.workspace\./,/^(?:vitest|jest)\.setup\./];function Ne(e){return!$(e)&&vn.some(n=>n.test(e))}var X=new Set(["node_modules","dist",".next","venv",".venv",".yarn","vendor","third_party","bower_components","out","coverage",".turbo",".vercel",".output",".svelte-kit",".nuxt",".cache",".pnpm-store","__pycache__","site-packages"]);function J(e){return e===".pnp.cjs"||e===".pnp.loader.mjs"||/\.min\.(?:js|mjs|cjs)$/.test(e)}var Q=512*1024;import{readdir as we,readFile as On}from"fs/promises";import{join as ee}from"path";var P=class e{constructor(n){this.rules=n}rules;static empty(){return new e([])}extend(n,t){let s=xn(t).map(r=>({...r,base:n}));return s.length===0?this:new e([...this.rules,...s])}ignores(n,t){let s=!1;for(let r of this.rules){if(r.dirOnly&&!t||s!==r.negate)continue;let a=Fn(r.base,n);a===null||!r.glob||Nn(r.glob,a)&&(s=!r.negate)}return s}};function xn(e){let n=[];for(let t of e.split(/\r?\n/)){if(n.length>=1e3)break;if(t.length>1024)continue;let s=Rn(t);s&&n.push(s)}return n}function Rn(e){let n=Tn(e);if(n===""||n.startsWith("#"))return null;let t=!1;n.startsWith("!")?(t=!0,n=n.slice(1)):(n.startsWith("\\!")||n.startsWith("\\#"))&&(n=n.slice(1));let s=!1;if(n.endsWith("/")&&(s=!0,n=n.replace(/\/+$/,"")),n==="")return null;let r=n.includes("/");return n=n.replace(/^\/+/,""),!r&&!n.startsWith("**/")&&(n=`**/${n}`),{glob:In(n),negate:t,dirOnly:s,source:e}}function Tn(e){let n=e.length;for(;n>0&&e[n-1]===" "&&e[n-2]!=="\\";)n--;return e.slice(0,n)}var ke=e=>e!=="/",Ae=()=>!0,bn=e=>e==="/";function In(e){let n=An(e);if(!n)return null;let t=["accept"],s=0;for(let o=n.length-1;o>=0;o--){let i=n[o],l=t.length;switch(i.kind){case"literal":{let c=i.ch;t.push({test:u=>u===c,next:s});break}case"one":t.push({test:i.test,next:s});break;case"star":case"rest":t.push({a:l+1,b:s},{test:i.kind==="star"?ke:Ae,next:l});break;case"dirs":t.push({a:l+1,b:s},{a:l+2,b:l+3},{test:Ae,next:l+1},{test:bn,next:s});break}s=l}let r="";for(let o of n){if(o.kind!=="literal")break;r+=o.ch}let a="";for(let o=n.length-1;o>=0;o--){let i=n[o];if(i.kind!=="literal")break;a=i.ch+a}return{states:t,start:s,prefix:r,suffix:a}}function Nn(e,n){if(!n.startsWith(e.prefix)||!n.endsWith(e.suffix))return!1;let{states:t}=e,s=new Int32Array(t.length).fill(-1),r=[],a=0,o=(i,l)=>{let c=[l];for(;c.length>0;){let u=c.pop();if(s[u]===a)continue;s[u]=a;let p=t[u];typeof p=="object"&&"a"in p?c.push(p.b,p.a):i.push(u)}};o(r,e.start);for(let i=0;i<n.length&&r.length>0;i++){let l=n[i];a++;let c=[];for(let u of r){let p=t[u];typeof p=="object"&&"test"in p&&p.test(l)&&o(c,p.next)}r=c}return r.some(i=>t[i]==="accept")}function An(e){let n=[],t=0;for(;t<e.length;){let s=e[t];if(s==="*"){if(e[t+1]==="*"){let r=t===0,a=r||e[t-1]==="/",o=e[t+2]==="/",i=t+2===e.length;if(a&&o){n.push({kind:"dirs"}),t+=3;continue}if(a&&i&&!r){n.push({kind:"rest"}),t+=2;continue}n.push({kind:"star"}),t+=2;continue}n.push({kind:"star"}),t++;continue}if(s==="?"){n.push({kind:"one",test:ke}),t++;continue}if(s==="["){let r=wn(e,t);if(!r)return null;n.push({kind:"one",test:r.test}),t=r.end;continue}if(s==="\\"&&t+1<e.length){n.push({kind:"literal",ch:e[t+1]}),t+=2;continue}n.push({kind:"literal",ch:s}),t++}return n}var kn={alnum:e=>/[A-Za-z0-9]/.test(e),alpha:e=>/[A-Za-z]/.test(e),blank:e=>e===" "||e===" ",cntrl:e=>e.charCodeAt(0)<32||e.charCodeAt(0)===127,digit:e=>e>="0"&&e<="9",graph:e=>e.charCodeAt(0)>32&&e.charCodeAt(0)<127,lower:e=>e>="a"&&e<="z",print:e=>e.charCodeAt(0)>=32&&e.charCodeAt(0)<127,punct:e=>/[!-/:-@[-`{-~]/.test(e),space:e=>/[ \t\n\r\f\v]/.test(e),upper:e=>e>="A"&&e<="Z",xdigit:e=>/[0-9A-Fa-f]/.test(e)};function wn(e,n){let t=n+1,s=e[t]==="!"||e[t]==="^";s&&t++;let r=[];for(let o=!0;;o=!1){if(t>=e.length)return null;let i=e[t];if(i==="]"&&!o)break;if(i==="["&&e[t+1]===":"){let c=e.indexOf(":]",t+2);if(c!==-1){let u=kn[e.slice(t+2,c)];if(!u)return null;r.push(u),t=c+2;continue}}if(i==="\\"){if(t++,t>=e.length)return null;i=e[t]}if(e[t+1]==="-"&&t+2<e.length&&e[t+2]!=="]"){let c=t+2;if(e[c]==="\\"&&c++,c>=e.length)return null;let[u,p]=[i,e[c]];r.push(d=>d>=u&&d<=p),t=c+1;continue}let l=i;r.push(c=>c===l),t++}return{test:o=>o!=="/"&&r.some(i=>i(o))!==s,end:t+1}}function Fn(e,n){return e===""?n:n.startsWith(`${e}/`)?n.slice(e.length+1):null}var Fe=new Set([".git",...X]);async function Oe(e,n){let t=n.skipDirs??Fe,s=n.respectGitignore??!0,r=[],a=[],o=[],i=[],l=P.empty().extend("",(n.exclude??[]).join(`
|
|
6
|
+
`));async function c(p,d,E){let T=p===""?e:ee(e,p),_;try{_=await we(T,{withFileTypes:!0})}catch(m){a.push({file:p||".",message:`cannot read directory: ${m.message}`});return}s&&_.some(m=>m.name===".gitignore"&&m.isFile())&&(d=d.extend(p,await On(ee(T,".gitignore"),"utf8")));for(let m of _){if(m.isSymbolicLink())continue;let v=p===""?m.name:`${p}/${m.name}`;if(m.isDirectory()){if(l.ignores(v,!0)||d.ignores(v,!0))continue;if(t.has(m.name)){X.has(m.name)&&!E&&await u(v)&&i.push(`${v}/`);continue}await c(v,d,E||n.skipTests===!0&&Ie.has(m.name))}else if(m.isFile()){if(!n.include(v,m.name)||l.ignores(v,!1)||d.ignores(v,!1)&&!n.keepIgnored?.(m.name))continue;J(m.name)?E||i.push(v):n.skipTests&&(E||Ne(m.name))?o.push(v):r.push(v)}}}async function u(p){let d;try{d=await we(ee(e,p),{withFileTypes:!0})}catch{return!1}for(let E of d){let T=`${p}/${E.name}`;if(E.isFile()&&n.include(T,E.name))return!0}for(let E of d)if(E.isDirectory()&&!E.isSymbolicLink()&&await u(`${p}/${E.name}`))return!0;return!1}return await c("",P.empty(),!1),r.sort(),o.sort(),i.sort(),{files:r,warnings:a,testFiles:o,vendored:i}}async function Ce(e,n={}){let{files:t,warnings:s,testFiles:r,vendored:a}=await Oe(e,{include:Cn,exclude:n.exclude,keepIgnored:$,skipTests:!n.includeTests});return Dn(t,{read:i=>Ln(Le(e,i),"utf8"),size:async i=>(await $n(Le(e,i))).size},s,n,r,a)}function Cn(e,n){return $(n)||j(n)||be.has(ne.extname(e).toLowerCase())}var Pn={quote:"a quote that never closes: the rest of the file was read as this value",block:"a -----BEGIN block with no -----END line: the rest of the file was skipped"};async function Dn(e,{read:n,size:t},s,r,a,o){let i=[],l=[],c=[],u=[];for(let f of e){let g=ne.basename(f);if(!$(g)){await t(f)>Q?u.push(f):(j(g)?c:l).push(f);continue}let{entries:R,commented:N,invalid:k,unterminated:O}=le(await n(f));for(let{line:U}of k)s.push({file:f,line:U,message:"ignored a line that is not KEY=value"});O&&s.push({file:f,line:O.line,message:Pn[O.kind]}),i.push({path:f,name:g,entries:R,commented:pe(g)?N:[]})}let p=new Set(i.map(f=>D(f.path))),d=new Map;for(let f of l){let g=Y(f);if(!g)continue;let R=q(await n(f),g,f);if(R.length===0)continue;let N=$e(D(f),p),k=d.get(N);if(k)for(let O of R)k.push(O);else d.set(N,R)}let E=new Map,T=(f,g)=>{let R=$e(D(f),p),N=E.get(R);N||E.set(R,N=new Set);for(let k of g)N.add(k)};for(let f of a){let g=Y(f);!g||await t(f)>Q||T(f,q(await n(f),g,f).map(R=>R.name))}for(let f of c)T(f,ie(await n(f)));let _=z(r.ignore??[]),m=z(r.defaultIgnore===!1?[]:oe),v=f=>_(f)||m(f),h=new Set;for(let f of d.values())for(let g of f)h.add(g.name);for(let f of i)for(let g of f.entries)h.add(g.key);let y=[...h].filter(f=>m(f)&&!_(f)).sort(),x=new Map;for(let f of i){let g=D(f.path),R=x.get(g);R?R.push(f):x.set(g,[f])}let b=f=>x.get(f)??[],S=[...new Set([...p,...d.keys()])].sort(),L=S.map(f=>{let g=H(b(f).map(R=>R.name));return g.length>64&&s.push({file:f||".",message:`${g.length} env files; only the first ${64} are reported`}),{scope:f,env_files:g.slice(0,64)}}),I=S.flatMap(f=>Ee({references:d.get(f)??[],envFiles:b(f),isIgnored:v,usedOutsideCode:E.get(f)})).sort(ye),F=S.flatMap(f=>_e({scope:f,references:d.get(f)??[],envFiles:b(f),isIgnored:v}).map(g=>({...g,defined_in:g.defined_in.slice(0,64)}))),C=S.flatMap(f=>(d.get(f)??[]).filter(g=>!v(g.name)).map(g=>({...g,scope:f}))).sort((f,g)=>f.file.localeCompare(g.file)||f.line-g.line||f.column-g.column);return{findings:I,references:C,variables:F,counts:Se(I),scopes:S,envScopes:L,defaultIgnored:y,testFilesSkipped:a.length,vendoredSkipped:o,tooLargeSkipped:u,sourceFiles:l.length,envFiles:i.map(f=>f.path),warnings:s}}function D(e){let n=ne.dirname(e);return n==="."?"":n}function $e(e,n){for(let t=e;t!=="";t=D(t))if(n.has(t))return t;return""}var Pe="DEPLOYHEALTH_TOKEN";var De="0.3.1";var A={ok:0,failed:1,usage:2},Ue=`Usage: deployhealth-scan [options]
|
|
6
7
|
|
|
7
8
|
Scan a repository for env var drift and report it to deployhealth.
|
|
8
9
|
|
|
9
10
|
Options:
|
|
10
11
|
--url <url> deployhealth base URL (required unless --dry-run)
|
|
11
|
-
--token <token> the project's ingest token, dh_... (required unless --dry-run
|
|
12
|
+
--token <token> the project's ingest token, dh_... (required unless --dry-run;
|
|
13
|
+
default: the DEPLOYHEALTH_TOKEN environment variable)
|
|
12
14
|
--sha <sha> commit being deployed (default: git rev-parse HEAD)
|
|
13
15
|
--branch <name> branch being deployed (default: current git branch)
|
|
14
16
|
--dir <path> directory to scan (default: current directory)
|
|
@@ -23,17 +25,18 @@ Options:
|
|
|
23
25
|
--json with --dry-run, print JSON
|
|
24
26
|
-v, --version print the version
|
|
25
27
|
-h, --help show this help
|
|
26
|
-
`,
|
|
28
|
+
`,Bn={url:{type:"string"},token:{type:"string"},sha:{type:"string"},branch:{type:"string"},dir:{type:"string"},ignore:{type:"string",multiple:!0},"no-default-ignore":{type:"boolean"},"include-tests":{type:"boolean"},exclude:{type:"string",multiple:!0},"dry-run":{type:"boolean"},json:{type:"boolean"},version:{type:"boolean",short:"v"},help:{type:"boolean",short:"h"}};async function Ge(e,n){let t;try{({values:t}=Gn({args:[...e],options:Bn,strict:!0,allowPositionals:!1}))}catch(_){return n.stderr(`deployhealth-scan: ${_.message}
|
|
27
29
|
|
|
28
|
-
${
|
|
29
|
-
`),
|
|
30
|
-
`);if(t["dry-run"])return n.stdout(t.json?`${JSON.stringify(
|
|
31
|
-
`:
|
|
32
|
-
`),
|
|
33
|
-
`),
|
|
34
|
-
`),
|
|
35
|
-
`),
|
|
36
|
-
`),
|
|
37
|
-
`),
|
|
38
|
-
`)}
|
|
30
|
+
${Ue}`),A.usage}if(t.help)return n.stdout(Ue),A.ok;if(t.version)return n.stdout(`${De}
|
|
31
|
+
`),A.ok;let s=Hn(n.cwd,t.dir??"."),r=await Ce(s,{ignore:t.ignore,exclude:t.exclude,defaultIgnore:!t["no-default-ignore"],includeTests:t["include-tests"]});for(let _ of r.warnings)n.stderr(`warning ${_.file}${_.line?`:${_.line}`:""}: ${_.message}
|
|
32
|
+
`);if(t["dry-run"])return n.stdout(t.json?`${JSON.stringify(jn(r),null,2)}
|
|
33
|
+
`:zn(r)),A.ok;let a=t.token||n.env.DEPLOYHEALTH_TOKEN||void 0;if(!t.url||!a)return n.stderr(`deployhealth-scan: --url and --token (or ${Pe}) are required (or pass --dry-run)
|
|
34
|
+
`),A.usage;Mn(t.url)&&n.stderr(`deployhealth-scan: warning: --url is plain http; the token and the report are sent unencrypted
|
|
35
|
+
`);let o=t.sha??n.git(["rev-parse","HEAD"],s),i=t.branch??n.git(["rev-parse","--abbrev-ref","HEAD"],s);if(!o||!i||i==="HEAD")return n.stderr(`deployhealth-scan: could not determine the commit and branch from git; pass --sha and --branch
|
|
36
|
+
`),A.usage;if(!de.test(o))return n.stderr(`deployhealth-scan: --sha must be a hex commit id, got "${o}"
|
|
37
|
+
`),A.usage;let l={sha:o,branch:i,timestamp:n.now().toISOString(),findings:r.findings,variables:r.variables,env_scopes:r.envScopes},c=`${t.url.replace(/\/+$/,"")}/api/ingest/scan`,u;try{u=await n.fetch(c,{method:"POST",headers:{authorization:`Bearer ${a}`,"content-type":"application/json"},body:JSON.stringify(l)})}catch(_){return n.stderr(`deployhealth-scan: could not reach ${c}: ${_.message}
|
|
38
|
+
`),A.failed}if(!u.ok){let _=(await u.text()).slice(0,500);return n.stderr(`deployhealth-scan: ingest failed with HTTP ${u.status}: ${_}
|
|
39
|
+
`),A.failed}let p=await u.json(),{missing:d,unused:E,mismatch:T}=p.counts;return n.stdout(`deployhealth: reported ${o.slice(0,7)} on ${i}: ${d} missing, ${E} unused, ${T} mismatch (deploy ${p.deployId})
|
|
40
|
+
`),A.ok}function Mn(e){let n;try{n=new URL(e)}catch{return!1}if(n.protocol!=="http:")return!1;let t=n.hostname.replace(/^\[|\]$/g,"").toLowerCase();return!(t==="localhost"||t.endsWith(".localhost")||/^127(?:\.\d{1,3}){3}$/.test(t)||t==="::1")}function jn(e){return{counts:e.counts,scopes:e.scopes,env_scopes:e.envScopes,findings:e.findings,variables:e.variables,default_ignored:e.defaultIgnored,test_files_skipped:e.testFilesSkipped,vendored_skipped:e.vendoredSkipped,too_large_skipped:e.tooLargeSkipped,warnings:e.warnings}}var Wn={missing:"MISSING",unused:"UNUSED",mismatch:"MISMATCH"};function zn(e){let n=e.scopes.map(r=>r||"(root)").join(", "),t=[`deployhealth-scan: ${e.sourceFiles} source files, scopes: ${n}`,""];for(let r of["missing","unused","mismatch"]){let a=e.findings.filter(o=>o.kind===r);t.push(`${Wn[r]} (${e.counts[r]})`),a.length===0&&t.push(" none");for(let o of a){let i=o.file?`${o.file}${o.line?`:${o.line}`:""}`:"",l=r==="mismatch"?` (missing from ${o.env_file})`:"";t.push(` ${o.var_name.padEnd(28)} ${i}${l}`)}t.push("")}let s=e.variables.filter(r=>r.optional&&r.defined_in.length===0);if(s.length>0){t.push(`OPTIONAL (${s.length}): a default in code, not defined in an env file`);for(let r of s)t.push(` ${r.var_name.padEnd(28)} ${r.scope||"(root)"}`);t.push("")}for(let{scope:r}of e.envScopes.filter(a=>a.env_files.length===0)){let a=e.variables.filter(o=>o.scope===r).length;a!==0&&(t.push(`No .env.example in ${r||"the repository root"}: ${a} variable${a===1?"":"s"} referenced (--json lists them).`),t.push(""))}if(e.testFilesSkipped>0){let r=e.testFilesSkipped===1?"file":"files";t.push(`Skipped ${e.testFilesSkipped} test and fixture ${r} (read only to see which variables they use). --include-tests includes them.`),t.push("")}if(e.vendoredSkipped.length>0&&(t.push(`Skipped vendored and generated code: ${He(e.vendoredSkipped)}.`),t.push("")),e.tooLargeSkipped.length>0){let r=e.tooLargeSkipped.length;t.push(`Skipped ${r} file${r===1?"":"s"} over 512 KB (bundles, not code people wrote): ${He(e.tooLargeSkipped)}.`),t.push("")}return e.defaultIgnored.length>0&&(t.push(`Skipped (the platform or runtime provides them): ${e.defaultIgnored.join(", ")}. --no-default-ignore includes them.`),t.push("")),`${t.join(`
|
|
41
|
+
`)}`}function He(e,n=5){let t=e.length-n;return t>0?`${e.slice(0,n).join(", ")} and ${t} more (--json lists them)`:e.join(", ")}var Be=()=>({cwd:process.cwd(),stdout:e=>process.stdout.write(e),stderr:e=>process.stderr.write(e),fetch:globalThis.fetch,git:(e,n)=>{try{return Un("git",e,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}},now:()=>new Date,env:{DEPLOYHEALTH_TOKEN:process.env.DEPLOYHEALTH_TOKEN}});Ge(process.argv.slice(2),Be()).then(e=>{process.exitCode=e},e=>{process.stderr.write(`deployhealth-scan: unexpected error: ${e instanceof Error?e.stack:String(e)}
|
|
39
42
|
`),process.exitCode=1});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deployhealth-scan",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Find env var drift in a repo (read in code but never defined, defined but never read, out of sync between env files) and report it to deployhealth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Utsav Mishra",
|