deployhealth-scan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/deployhealth-scan.mjs +34 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Utsav Mishra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# deployhealth-scan
|
|
2
|
+
|
|
3
|
+
Find env var drift in a repository and report it to
|
|
4
|
+
[deployhealth](https://github.com/Utsav-mishra-25/deployhealth):
|
|
5
|
+
|
|
6
|
+
- **MISSING**: read in code (`process.env.X`, `os.environ["X"]`, `os.Getenv("X")`, `ENV["X"]`) but
|
|
7
|
+
defined in no env file;
|
|
8
|
+
- **UNUSED**: defined in an env file but never read;
|
|
9
|
+
- **MISMATCH**: in `.env` but not `.env.example`, or the reverse.
|
|
10
|
+
|
|
11
|
+
It scans JavaScript/TypeScript, Python, Go and Ruby, respects `.gitignore`, and treats each folder
|
|
12
|
+
with its own `.env.example` as a separate scope (monorepos). One file, no dependencies, Node 20+.
|
|
13
|
+
|
|
14
|
+
## Try it locally
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx deployhealth-scan@0.1.0 --dry-run # grouped findings with file:line
|
|
18
|
+
npx deployhealth-scan@0.1.0 --dry-run --json # the same, as JSON
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With `--dry-run` nothing leaves your machine.
|
|
22
|
+
|
|
23
|
+
## Report every push from GitHub Actions
|
|
24
|
+
|
|
25
|
+
Create a project in deployhealth, add its ingest token as the repository secret
|
|
26
|
+
`DEPLOYHEALTH_TOKEN`, then add `.github/workflows/deployhealth.yml`:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
name: deployhealth
|
|
30
|
+
|
|
31
|
+
# Push events only. The token is a secret, so never run this on pull_request events from forks.
|
|
32
|
+
on:
|
|
33
|
+
push:
|
|
34
|
+
branches: [main]
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
env-scan:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v5
|
|
43
|
+
- uses: actions/setup-node@v5
|
|
44
|
+
with:
|
|
45
|
+
node-version: 22
|
|
46
|
+
- name: Scan env vars and report to deployhealth
|
|
47
|
+
env:
|
|
48
|
+
DEPLOYHEALTH_TOKEN: ${{ secrets.DEPLOYHEALTH_TOKEN }}
|
|
49
|
+
run: |
|
|
50
|
+
npx --yes deployhealth-scan@0.1.0 \
|
|
51
|
+
--url https://deployhealth-production.up.railway.app \
|
|
52
|
+
--token "$DEPLOYHEALTH_TOKEN" \
|
|
53
|
+
--sha "${{ github.sha }}" \
|
|
54
|
+
--branch "${{ github.ref_name }}"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pin the version (`@0.1.0`) so an update never runs in your CI unreviewed. For a self-hosted
|
|
58
|
+
deployhealth, change `--url` to your instance.
|
|
59
|
+
|
|
60
|
+
## What it sends
|
|
61
|
+
|
|
62
|
+
The commit sha and branch, and for each finding the variable name, `file:line` and env file name,
|
|
63
|
+
plus the list of variable names each env file defines. **Never values:** the scanner reads env
|
|
64
|
+
files for the names they define; values are neither sent nor printed.
|
|
65
|
+
|
|
66
|
+
## Options
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
--url <url> deployhealth base URL (required unless --dry-run)
|
|
70
|
+
--token <token> the project's ingest token, dh_... (required unless --dry-run)
|
|
71
|
+
--sha <sha> commit being deployed (default: git rev-parse HEAD)
|
|
72
|
+
--branch <name> branch being deployed (default: current git branch)
|
|
73
|
+
--dir <path> directory to scan (default: current directory)
|
|
74
|
+
--ignore <glob> skip variables matching a glob, e.g. NODE_ENV (repeatable)
|
|
75
|
+
--exclude <pattern> skip paths matching a gitignore-style pattern (repeatable)
|
|
76
|
+
--dry-run print the findings instead of sending them
|
|
77
|
+
--json with --dry-run, print JSON
|
|
78
|
+
-v, --version print the version
|
|
79
|
+
-h, --help show this help
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Exit codes: `0` success, `1` the report failed (network or HTTP error), `2` usage error.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT. The deployhealth web app and worker are licensed separately (FSL-1.1-MIT); see the
|
|
87
|
+
[repository](https://github.com/Utsav-mishra-25/deployhealth#licensing).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{execFileSync as de}from"child_process";import{resolve as fe}from"path";import{parseArgs as me}from"util";import{readFile as W}from"fs/promises";import{join as G,posix as b}from"path";import{readFile as Se}from"fs/promises";var Z=/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=(.*)$/,J=new Set(['"',"'","`"]);function R(n){let e=n.replace(/^\uFEFF/,"").split(/\r?\n/),t=[],s=[];for(let i=0;i<e.length;i++){let r=e[i]??"",l=r.trim();if(l===""||l.startsWith("#"))continue;let u=Z.exec(r);if(!u){s.push({line:i+1,text:r});continue}let a=u[1],c=(u[2]??"").trimStart(),p=i+1,d=c[0];if(d!==void 0&&J.has(d)){let o=X(e,i,c,d);if(o){t.push({key:a,value:o.value,line:p}),i=o.endIndex;continue}}t.push({key:a,value:ee(c).trim(),line:p})}return{entries:t,invalid:s}}function X(n,e,t,s){let i=t.slice(1),r=e;for(;;){let l=Q(i,s);if(l!==-1){let u=i.slice(0,l);return{value:s==='"'?Y(u):u,endIndex:r}}if(r++,r>=n.length)return null;i+=`
|
|
3
|
+
`+n[r]}}function Q(n,e){for(let t=0;t<n.length;t++){let s=n[t];if(s==="\\"){t++;continue}if(s===e)return t}return-1}function Y(n){return n.replace(/\\([nrt"\\])/g,(e,t)=>{switch(t){case"n":return`
|
|
4
|
+
`;case"r":return"\r";case"t":return" ";default:return t}})}function ee(n){let e=/(^|\s)#/.exec(n);return e?n.slice(0,e.index):n}var E=[".env.example",".env",".env.local"];var $=/^[0-9a-f]{7,64}$/i;function _({references:n,envFiles:e,isIgnored:t}){let s=[],i=new Set(e.flatMap(a=>a.entries.map(c=>c.key))),r=new Set(n.map(a=>a.name));for(let a of n)t(a.name)||i.has(a.name)||s.push({kind:"missing",var_name:a.name,file:a.file,line:a.line,env_file:null});for(let a of e)for(let{key:c,line:p}of F(a.entries))t(c)||r.has(c)||s.push({kind:"unused",var_name:c,file:a.path,line:p,env_file:a.path});let l=e.find(a=>a.name===".env"),u=e.find(a=>a.name===".env.example");if(l&&u)for(let[a,c]of[[l,u],[u,l]]){let p=new Set(c.entries.map(d=>d.key));for(let{key:d,line:o}of F(a.entries))t(d)||p.has(d)||s.push({kind:"mismatch",var_name:d,file:a.path,line:o,env_file:c.path})}return s}function F(n){let e=new Set;return n.filter(t=>e.has(t.key)?!1:(e.add(t.key),!0))}var k={missing:0,unused:1,mismatch:2};function I({scope:n,references:e,envFiles:t,isIgnored:s}){return[...new Set(e.map(r=>r.name))].filter(r=>!s(r)).sort().map(r=>({var_name:r,scope:n,defined_in:E.filter(l=>t.some(u=>u.name===l&&u.entries.some(a=>a.key===r)))}))}function A(n,e){return k[n.kind]-k[e.kind]||N(n.var_name,e.var_name)||N(n.file??"",e.file??"")||(n.line??0)-(e.line??0)}function P(n){let e={missing:new Set,unused:new Set,mismatch:new Set};for(let t of n)e[t.kind].add(t.var_name);return{missing:e.missing.size,unused:e.unused.size,mismatch:e.mismatch.size}}function N(n,e){return n<e?-1:n>e?1:0}function ne(n){let e="";for(let t of n)t==="*"?e+=".*":t==="?"?e+=".":e+=t.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp(`^${e}$`)}function O(n){let e=n.map(ne);return t=>e.some(s=>s.test(t))}import{extname as te}from"path";var T={".ts":"javascript",".tsx":"javascript",".js":"javascript",".jsx":"javascript",".py":"python",".go":"go",".rb":"ruby"},L=new Set(Object.keys(T)),S="(?<name>[A-Za-z_][A-Za-z0-9_]*)",h=n=>`(?<q>[${n}])${S}\\k<q>`,re={javascript:[{syntax:"process.env",regex:new RegExp(`\\bprocess\\.env(?:\\?\\.|\\.)${S}\\b`,"g")},{syntax:"process.env",regex:new RegExp(`\\bprocess\\.env(?:\\?\\.)?\\[\\s*${h("'\"`")}\\s*\\]`,"g")},{syntax:"import.meta.env",regex:new RegExp(`\\bimport\\.meta\\.env(?:\\?\\.|\\.)${S}\\b`,"g")},{syntax:"import.meta.env",regex:new RegExp(`\\bimport\\.meta\\.env(?:\\?\\.)?\\[\\s*${h("'\"`")}\\s*\\]`,"g")}],python:[{syntax:"os.environ",regex:new RegExp(`\\bos\\.environ\\s*\\[\\s*${h(`'"`)}\\s*\\]`,"g")},{syntax:"os.environ",regex:new RegExp(`\\bos\\.environ\\.get\\s*\\(\\s*${h(`'"`)}`,"g")},{syntax:"os.getenv",regex:new RegExp(`\\bos\\.getenv\\s*\\(\\s*${h(`'"`)}`,"g")}],go:[{syntax:"os.Getenv",regex:new RegExp(`\\bos\\.Getenv\\s*\\(\\s*${h('"`')}\\s*\\)`,"g")},{syntax:"os.LookupEnv",regex:new RegExp(`\\bos\\.LookupEnv\\s*\\(\\s*${h('"`')}\\s*\\)`,"g")}],ruby:[{syntax:"ENV",regex:new RegExp(`\\bENV\\s*\\[\\s*${h(`'"`)}\\s*\\]`,"g")},{syntax:"ENV",regex:new RegExp(`\\bENV\\.fetch(?:\\s*\\(\\s*|\\s+)${h(`'"`)}`,"g")}]};function j(n){return T[te(n).toLowerCase()]}function V(n,e,t){let s=[],i=n.split(/\r?\n/);for(let r=0;r<i.length;r++){let l=i[r];for(let{syntax:u,regex:a}of re[e]){a.lastIndex=0;for(let c=a.exec(l);c;c=a.exec(l)){let p=c.groups?.name;p&&s.push({name:p,file:t,line:r+1,column:c.index+1,syntax:u})}}}return s.sort((r,l)=>r.line-l.line||r.column-l.column)}import{readdir as le,readFile as ue}from"fs/promises";import{join as C}from"path";var y=class n{constructor(e){this.rules=e}rules;static empty(){return new n([])}extend(e,t){let s=se(t).map(i=>({...i,base:e}));return s.length===0?this:new n([...this.rules,...s])}ignores(e,t){let s=!1;for(let i of this.rules){if(i.dirOnly&&!t)continue;let r=ce(i.base,e);r!==null&&i.regex.test(r)&&(s=!i.negate)}return s}};function se(n){let e=[];for(let t of n.split(/\r?\n/)){let s=ie(t);s&&e.push(s)}return e}function ie(n){let e=oe(n);if(e===""||e.startsWith("#"))return null;let t=!1;e.startsWith("!")?(t=!0,e=e.slice(1)):(e.startsWith("\\!")||e.startsWith("\\#"))&&(e=e.slice(1));let s=!1;if(e.endsWith("/")&&(s=!0,e=e.replace(/\/+$/,"")),e==="")return null;let i=e.includes("/");return e=e.replace(/^\/+/,""),!i&&!e.startsWith("**/")&&(e=`**/${e}`),{regex:new RegExp(`^${ae(e)}$`),negate:t,dirOnly:s,source:n}}function oe(n){let e=n.length;for(;e>0&&n[e-1]===" "&&n[e-2]!=="\\";)e--;return n.slice(0,e)}function ae(n){let e="",t=0;for(;t<n.length;){let s=n[t];if(s==="*"){if(n[t+1]==="*"){let i=t===0,r=i||n[t-1]==="/",l=n[t+2]==="/",u=t+2===n.length;if(r&&l){e+="(?:.*/)?",t+=3;continue}if(r&&u&&!i){e+=".*",t+=2;continue}e+="[^/]*",t+=2;continue}e+="[^/]*",t++;continue}if(s==="?"){e+="[^/]",t++;continue}if(s==="["){let i=n.indexOf("]",t+2);if(i!==-1){let r=n.slice(t+1,i);r.startsWith("!")&&(r=`^${r.slice(1)}`),e+=`[${r.replace(/\\/g,"\\\\")}]`,t=i+1;continue}}if(s==="\\"&&t+1<n.length){e+=D(n[t+1]),t+=2;continue}e+=D(s),t++}return e}function D(n){return n.replace(/[.*+?^${}()|[\]\\/]/g,"\\$&")}function ce(n,e){return n===""?e:e.startsWith(`${n}/`)?e.slice(n.length+1):null}var ge=new Set(["node_modules","dist",".git",".next","venv",".venv"]);async function M(n,e){let t=e.skipDirs??ge,s=e.respectGitignore??!0,i=[],r=[],l=y.empty().extend("",(e.exclude??[]).join(`
|
|
5
|
+
`));async function u(a,c){let p=a===""?n:C(n,a),d;try{d=await le(p,{withFileTypes:!0})}catch(o){r.push({file:a||".",message:`cannot read directory: ${o.message}`});return}s&&d.some(o=>o.name===".gitignore"&&o.isFile())&&(c=c.extend(a,await ue(C(p,".gitignore"),"utf8")));for(let o of d){if(o.isSymbolicLink())continue;let g=a===""?o.name:`${a}/${o.name}`;if(o.isDirectory()){if(t.has(o.name)||l.ignores(g,!0)||c.ignores(g,!0))continue;await u(g,c)}else if(o.isFile()){if(!e.include(g,o.name)||l.ignores(g,!1)||c.ignores(g,!1)&&!e.keepIgnored?.(o.name))continue;i.push(g)}}}return await u("",y.empty()),i.sort(),{files:i,warnings:r}}var w=new Set(E);async function q(n,e={}){let{files:t,warnings:s}=await M(n,{include:(o,g)=>w.has(g)||L.has(b.extname(o).toLowerCase()),exclude:e.exclude,keepIgnored:o=>w.has(o)}),i=[],r=[];for(let o of t){let g=b.basename(o);if(!w.has(g)){r.push(o);continue}let{entries:f,invalid:v}=R(await W(G(n,o),"utf8"));for(let{line:U}of v)s.push({file:o,line:U,message:"ignored a line that is not KEY=value"});i.push({path:o,name:g,entries:f})}let l=new Set(i.map(o=>x(o.path))),u=new Map;for(let o of r){let g=j(o);if(!g)continue;let f=V(await W(G(n,o),"utf8"),g,o);if(f.length===0)continue;let v=pe(x(o),l);u.set(v,[...u.get(v)??[],...f])}let a=O(e.ignore??[]),c=[...new Set([...l,...u.keys()])].sort(),p=c.flatMap(o=>_({references:u.get(o)??[],envFiles:i.filter(g=>x(g.path)===o),isIgnored:a})).sort(A),d=c.flatMap(o=>I({scope:o,references:u.get(o)??[],envFiles:i.filter(g=>x(g.path)===o),isIgnored:a}));return{findings:p,variables:d,counts:P(p),scopes:c,sourceFiles:r.length,envFiles:i.map(o=>o.path),warnings:s}}function x(n){let e=b.dirname(n);return e==="."?"":e}function pe(n,e){for(let t=n;t!=="";t=x(t))if(e.has(t))return t;return""}var z="0.1.0";var m={ok:0,failed:1,usage:2},K=`Usage: deployhealth-scan [options]
|
|
6
|
+
|
|
7
|
+
Scan a repository for env var drift and report it to deployhealth.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--url <url> deployhealth base URL (required unless --dry-run)
|
|
11
|
+
--token <token> the project's ingest token, dh_... (required unless --dry-run)
|
|
12
|
+
--sha <sha> commit being deployed (default: git rev-parse HEAD)
|
|
13
|
+
--branch <name> branch being deployed (default: current git branch)
|
|
14
|
+
--dir <path> directory to scan (default: current directory)
|
|
15
|
+
--ignore <glob> skip variables matching a glob, e.g. NODE_ENV (repeatable)
|
|
16
|
+
--exclude <pattern> skip paths matching a gitignore-style pattern (repeatable)
|
|
17
|
+
--dry-run print the findings instead of sending them
|
|
18
|
+
--json with --dry-run, print JSON
|
|
19
|
+
-v, --version print the version
|
|
20
|
+
-h, --help show this help
|
|
21
|
+
`,he={url:{type:"string"},token:{type:"string"},sha:{type:"string"},branch:{type:"string"},dir:{type:"string"},ignore:{type:"string",multiple:!0},exclude:{type:"string",multiple:!0},"dry-run":{type:"boolean"},json:{type:"boolean"},version:{type:"boolean",short:"v"},help:{type:"boolean",short:"h"}};async function B(n,e){let t;try{({values:t}=me({args:[...n],options:he,strict:!0,allowPositionals:!1}))}catch(f){return e.stderr(`deployhealth-scan: ${f.message}
|
|
22
|
+
|
|
23
|
+
${K}`),m.usage}if(t.help)return e.stdout(K),m.ok;if(t.version)return e.stdout(`${z}
|
|
24
|
+
`),m.ok;let s=fe(e.cwd,t.dir??"."),i=await q(s,{ignore:t.ignore,exclude:t.exclude});for(let f of i.warnings)e.stderr(`warning ${f.file}${f.line?`:${f.line}`:""}: ${f.message}
|
|
25
|
+
`);if(t["dry-run"])return e.stdout(t.json?`${JSON.stringify(ye(i),null,2)}
|
|
26
|
+
`:ve(i)),m.ok;if(!t.url||!t.token)return e.stderr(`deployhealth-scan: --url and --token are required (or pass --dry-run)
|
|
27
|
+
`),m.usage;let r=t.sha??e.git(["rev-parse","HEAD"],s),l=t.branch??e.git(["rev-parse","--abbrev-ref","HEAD"],s);if(!r||!l||l==="HEAD")return e.stderr(`deployhealth-scan: could not determine the commit and branch from git; pass --sha and --branch
|
|
28
|
+
`),m.usage;if(!$.test(r))return e.stderr(`deployhealth-scan: --sha must be a hex commit id, got "${r}"
|
|
29
|
+
`),m.usage;let u={sha:r,branch:l,timestamp:e.now().toISOString(),findings:i.findings,variables:i.variables},a=`${t.url.replace(/\/+$/,"")}/api/ingest/scan`,c;try{c=await e.fetch(a,{method:"POST",headers:{authorization:`Bearer ${t.token}`,"content-type":"application/json"},body:JSON.stringify(u)})}catch(f){return e.stderr(`deployhealth-scan: could not reach ${a}: ${f.message}
|
|
30
|
+
`),m.failed}if(!c.ok){let f=(await c.text()).slice(0,500);return e.stderr(`deployhealth-scan: ingest failed with HTTP ${c.status}: ${f}
|
|
31
|
+
`),m.failed}let p=await c.json(),{missing:d,unused:o,mismatch:g}=p.counts;return e.stdout(`deployhealth: reported ${r.slice(0,7)} on ${l}: ${d} missing, ${o} unused, ${g} mismatch (deploy ${p.deployId})
|
|
32
|
+
`),m.ok}function ye(n){return{counts:n.counts,scopes:n.scopes,findings:n.findings,variables:n.variables,warnings:n.warnings}}var xe={missing:"MISSING",unused:"UNUSED",mismatch:"MISMATCH"};function ve(n){let e=n.scopes.map(s=>s||"(root)").join(", "),t=[`deployhealth-scan: ${n.sourceFiles} source files, scopes: ${e}`,""];for(let s of["missing","unused","mismatch"]){let i=n.findings.filter(r=>r.kind===s);t.push(`${xe[s]} (${n.counts[s]})`),i.length===0&&t.push(" none");for(let r of i){let l=r.file?`${r.file}${r.line?`:${r.line}`:""}`:"",u=s==="mismatch"?` (missing from ${r.env_file})`:"";t.push(` ${r.var_name.padEnd(28)} ${l}${u}`)}t.push("")}return`${t.join(`
|
|
33
|
+
`)}`}var H=()=>({cwd:process.cwd(),stdout:n=>process.stdout.write(n),stderr:n=>process.stderr.write(n),fetch:globalThis.fetch,git:(n,e)=>{try{return de("git",n,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()||null}catch{return null}},now:()=>new Date});B(process.argv.slice(2),H()).then(n=>{process.exitCode=n},n=>{process.stderr.write(`deployhealth-scan: unexpected error: ${n instanceof Error?n.stack:String(n)}
|
|
34
|
+
`),process.exitCode=1});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deployhealth-scan",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": "Utsav Mishra",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"deployhealth-scan": "dist/deployhealth-scan.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Utsav-mishra-25/deployhealth.git",
|
|
21
|
+
"directory": "packages/core/npm"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/Utsav-mishra-25/deployhealth/tree/main/packages/core/npm#readme",
|
|
24
|
+
"bugs": "https://github.com/Utsav-mishra-25/deployhealth/issues",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"env",
|
|
27
|
+
"environment-variables",
|
|
28
|
+
"dotenv",
|
|
29
|
+
"env-drift",
|
|
30
|
+
"ci",
|
|
31
|
+
"github-actions",
|
|
32
|
+
"deployhealth"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"prepack": "node -e \"require('node:fs').accessSync('dist/deployhealth-scan.mjs')\""
|
|
39
|
+
}
|
|
40
|
+
}
|