shipscanner 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.
Files changed (2) hide show
  1. package/dist/index.js +18 -0
  2. package/package.json +51 -0
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import{Command as de}from"commander";import ae from"ora";import I from"chalk";import L from"path";import X from"os";import y from"fs";var U=L.join(X.homedir(),".shipscanner"),v=L.join(U,"config.json"),k={apiUrl:"https://shipscanner.dev"};function F(){y.existsSync(U)||y.mkdirSync(U,{recursive:!0,mode:448})}function S(){if(F(),!y.existsSync(v))return{...k};try{let e=y.readFileSync(v,"utf-8");return{...k,...JSON.parse(e)}}catch{return{...k}}}function D(e){F(),y.writeFileSync(v,JSON.stringify(e,null,2),{mode:384})}function b(){return process.env.SHIPSCANNER_API_KEY||S().apiKey}function j(e){let n=S();n.apiKey=e,D(n)}function T(){let e=S();delete e.apiKey,D(e)}function d(){return process.env.SHIPSCANNER_API_URL||S().apiUrl}function _(e){let n=S();n.apiUrl=e,D(n)}function w(){return v}var f=class extends Error{constructor(r,o,l){super(o);this.statusCode=r;this.retryAfter=l;this.name="ApiError"}};function ee(){let e={"Content-Type":"application/json","User-Agent":"shipscanner-cli/0.1.0"},n=b();return n&&(e.Authorization=`Bearer ${n}`),e}async function E(e,n,r){let l=`${d()}${n}`,i=await fetch(l,{method:e,headers:ee(),body:r?JSON.stringify(r):void 0});if(i.status===429){let g=parseInt(i.headers.get("Retry-After")||"60",10);throw new f(429,`Rate limited. Try again in ${g} seconds. Run \`shipscanner login\` for higher limits.`,g)}let s=await i.json();if(i.status>=400)throw new f(i.status,s.message||`Request failed (${i.status})`);return s}async function K(e,n){let r={repoUrl:e};return n&&(r.branch=n),(await E("POST","/api/scan",r)).data}async function N(e){return(await E("GET",`/api/scan/${e}`)).data}async function G(e){return(await E("GET",`/api/scan/${e}/diff`)).data}async function J(e,n,r=36e4){let o=Date.now(),l=3e3;for(;Date.now()-o<r;){let i=await N(e);if(n&&n(i.status),i.status==="COMPLETED")return i;if(i.status==="FAILED")throw new Error(i.errorMessage||"Scan failed");await new Promise(s=>setTimeout(s,l))}throw new Error(`Scan timed out after ${r/1e3}s`)}import t from"chalk";import ne from"cli-table3";var re={"A+":t.bold.cyan,A:t.bold.cyan,B:t.bold.yellow,C:t.bold.hex("#f97316"),D:t.bold.red,F:t.bold.red},te={critical:t.bold.red,high:t.bold.hex("#f97316"),medium:t.bold.yellow,low:t.dim};function O(e){return e>=800?"A+":e>=700?"A":e>=600?"B":e>=500?"C":e>=400?"D":"F"}function M(e){return re[e]||t.white}function oe(e){return te[e]||t.white}function ie(e,n,r=20){let o=Math.max(0,Math.min(1,e/n)),l=Math.round(o*r),i=r-l,s;return o>=.8?s=t.cyan:o>=.6?s=t.yellow:o>=.4?s=t.hex("#f97316"):s=t.red,s("\u2588".repeat(l))+t.gray("\u2591".repeat(i))}function se(e){let n=O(e),r=M(n),o=40,l=(e-300)/550,i=Math.round(l*o),s=o-i,g=r("\u2588".repeat(i))+t.gray("\u2591".repeat(s));return["",t.bold(" Score: ")+r(`${e} / 850`)+" "+r(`(${n})`),` ${g}`,t.gray(` 300${"\u2500".repeat(o-6)}850`),""].join(`
3
+ `)}function x(e){let n=[],r=O(e.score),o=M(r);if(n.push(""),n.push(t.bold.white(" ShipScanner Report")),n.push(t.gray(` ${e.repoName} (${e.branch})`)),n.push(se(e.score)),e.categoryScores&&e.categoryScores.length>0){let c=new ne({chars:{top:"","top-mid":"","top-left":"","top-right":"",bottom:"","bottom-mid":"","bottom-left":"","bottom-right":"",left:" ","left-mid":"",mid:"","mid-mid":"",right:"","right-mid":"",middle:" "},style:{"padding-left":0,"padding-right":1}});for(let a of e.categoryScores){let m=ie(a.score,a.maxScore,15),P=Math.round(a.score/a.maxScore*100);c.push([t.white(a.label.padEnd(22)),m,t.white(`${a.score}/${a.maxScore}`),t.gray(`(${P}%)`),a.findingsCount>0?t.yellow(`${a.findingsCount} issues`):t.green("clean")])}n.push(c.toString()),n.push("")}let l=e.findings||[],i={critical:l.filter(c=>c.severity==="critical").length,high:l.filter(c=>c.severity==="high").length,medium:l.filter(c=>c.severity==="medium").length,low:l.filter(c=>c.severity==="low").length},s=[];i.critical>0&&s.push(t.bold.red(`Critical: ${i.critical}`)),i.high>0&&s.push(t.hex("#f97316")(`High: ${i.high}`)),i.medium>0&&s.push(t.yellow(`Medium: ${i.medium}`)),i.low>0&&s.push(t.gray(`Low: ${i.low}`)),s.length>0&&(n.push(" "+s.join(" ")),n.push(""));let g=l.filter(c=>c.severity==="critical"||c.severity==="high");if(g.length>0){n.push(t.bold(" Top Issues:"));let c=g.slice(0,10);for(let a=0;a<c.length;a++){let m=c[a],P=oe(m.severity)(`[${m.severity.toUpperCase()}]`),W=m.filePath?t.gray(` ${m.filePath}${m.line?`:${m.line}`:""}`):"";n.push(` ${t.gray(`${a+1}.`)} ${P} ${m.title}${W}`)}g.length>10&&n.push(t.gray(` ... and ${g.length-10} more critical/high issues`)),n.push("")}return n.join(`
4
+ `)}function R(e,n){let r=[];return r.push(t.gray(" Full report: ")+t.underline.cyan(`${n}/report/${e}`)),r.push(t.gray(" Badge: ")+t.gray(`![ShipScanner](${n}/api/badge/${e})`)),r.push(""),r.join(`
5
+ `)}function H(e){let n=[];if(e.isFirstScan||!e.previous||!e.delta)return n.push(t.gray(" First scan for this repo \u2014 no previous data to compare.")),n.join(`
6
+ `);let r=e.delta.score,o=r>0?t.green(`+${r} \u2191`):r<0?t.red(`${r} \u2193`):t.gray("0 \u2192");return n.push(""),n.push(t.bold(" Score Delta")),n.push(` Previous: ${e.previous.score} (${e.previous.grade}) \u2192 Current: ${e.current.score} (${e.current.grade}) ${o}`),e.resolvedCount>0&&n.push(t.green(` ${e.resolvedCount} findings resolved`)),e.newFindings.length>0&&n.push(t.yellow(` ${e.newFindings.length} new findings introduced`)),n.push(""),n.join(`
7
+ `)}function B(e,n){let r={id:e.id,repoName:e.repoName,branch:e.branch,score:e.score,grade:e.score?O(e.score):null,status:e.status,categories:e.categoryScores,findings:e.findings?.map(o=>({severity:o.severity,category:o.category,title:o.title,filePath:o.filePath,line:o.line,tool:o.tool,ruleId:o.ruleId})),summary:e.summary,reportUrl:`https://shipscanner.dev/report/${e.id}`};return n&&!n.isFirstScan&&n.delta&&(r.delta={score:n.delta.score,gradeChanged:n.delta.gradeChanged,newFindings:n.newFindings.length,resolvedFindings:n.resolvedCount}),JSON.stringify(r,null,2)}function u(e){return`
8
+ ${t.red("Error:")} ${e}
9
+ `}function $(e){return`
10
+ ${t.green("OK:")} ${e}
11
+ `}var ce={QUEUED:"Queued",CLONING:"Cloning repository",SCANNING:"Running scanners",SCORING:"Calculating score"};function le(e){let n=e.trim();/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(n)&&(n=`https://github.com/${n}`),n=n.replace(/\/+$/,"").replace(/\.git$/,"");let r=n.match(/^(https:\/\/github\.com\/[^/]+\/[^/]+)\/tree\/.+/);return r&&(n=r[1]),n}function ge(e){return/^https:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(e)}async function z(e,n){let r=le(e);ge(r)||(console.log(u(`Invalid GitHub URL: ${e}
12
+ Expected: https://github.com/owner/repo or owner/repo`)),process.exit(1));let o=r.replace("https://github.com/",""),l=n.json,i=l?null:ae({text:`Scanning ${I.cyan(o)}...`,color:"cyan"}).start();try{let{scanId:s}=await K(r,n.branch),g=await J(s,a=>{i&&(i.text=`${ce[a]||a}... ${I.gray(o)}`)});i&&i.stop();let c;if(!n.noDiff)try{c=await G(s)}catch{}if(n.json?console.log(B(g,c)):(console.log(x(g)),c&&console.log(H(c)),console.log(R(s,d()))),n.threshold){let a=parseInt(n.threshold,10);(isNaN(a)||a<300||a>850)&&(console.log(u("Threshold must be between 300 and 850")),process.exit(1)),g.score!==null&&g.score<a?(l||console.log(I.red(` Score ${g.score} is below threshold ${a}. Failing.
13
+ `)),process.exit(1)):l||console.log(I.green(` Score ${g.score} meets threshold ${a}.
14
+ `))}}catch(s){if(i&&i.stop(),s instanceof f)n.json?console.log(JSON.stringify({error:s.message,statusCode:s.statusCode})):console.log(u(s.message));else{let g=s instanceof Error?s.message:String(s);n.json?console.log(JSON.stringify({error:g})):console.log(u(g))}process.exit(1)}}import p from"chalk";import ue from"open";async function Y(e){if(e.key){let r=e.key.trim();r.startsWith("sk_")||(console.log(u("API key must start with 'sk_'. Get one at shipscanner.dev/settings.")),process.exit(1)),j(r),console.log($(`API key saved to ${w()}`)),console.log(p.gray(` You now have higher rate limits and access to private repo scanning.
15
+ `));return}let n=`${d()}/settings`;console.log(""),console.log(p.bold(" ShipScanner Login")),console.log(""),console.log(" To authenticate, create an API key at:"),console.log(` ${p.underline.cyan(n)}`),console.log(""),console.log(" Then run:"),console.log(p.white(" shipscanner login --key sk_your_api_key")),console.log("");try{await ue(n),console.log(p.gray(` (Opening browser...)
16
+ `))}catch{console.log(p.gray(` (Could not open browser \u2014 visit the URL manually)
17
+ `))}}async function q(){if(!b()){console.log(u("Not logged in. No API key found."));return}T(),console.log($("API key removed. You are now logged out."))}async function Z(){let e=b(),n=process.env.SHIPSCANNER_API_KEY?"environment variable":"config file";if(!e){console.log(""),console.log(p.gray(" Not authenticated.")),console.log(p.gray(` Run ${p.white("shipscanner login")} to authenticate.
18
+ `));return}let r=e.substring(0,10)+"...";console.log(""),console.log(p.bold(" ShipScanner Auth")),console.log(` API Key: ${p.cyan(r)} (from ${n})`),console.log(` API URL: ${p.gray(d())}`),console.log(` Config: ${p.gray(w())}`),console.log("")}import pe from"ora";import C from"chalk";async function V(e,n){let r=n.json?null:pe({text:"Fetching scan status...",color:"cyan"}).start();try{let o=await N(e);if(r&&r.stop(),n.json){console.log(JSON.stringify(o,null,2));return}o.status==="COMPLETED"?(console.log(x(o)),console.log(R(e,d()))):o.status==="FAILED"?console.log(u(`Scan failed: ${o.errorMessage||"Unknown error"}`)):(console.log(""),console.log(C.bold(" Scan Status")),console.log(` ID: ${C.gray(e)}`),console.log(` Repo: ${C.cyan(o.repoName)}`),console.log(` Status: ${C.yellow(o.status)}`),console.log(""),console.log(C.gray(" Scan is still in progress. Run this command again to check.")),console.log(""))}catch(o){r&&r.stop(),o instanceof f?console.log(u(o.message)):console.log(u(o instanceof Error?o.message:String(o))),process.exit(1)}}import A from"chalk";async function Q(e){if(e.apiUrl){try{new URL(e.apiUrl)}catch{console.log(u(`Invalid URL: ${e.apiUrl}`)),process.exit(1)}_(e.apiUrl.replace(/\/+$/,"")),console.log($(`API URL set to ${e.apiUrl}`));return}console.log(""),console.log(A.bold(" ShipScanner Config")),console.log(` API URL: ${A.cyan(d())}`),console.log(` Config: ${A.gray(w())}`),console.log(""),console.log(A.gray(" Options:")),console.log(A.gray(" --api-url <url> Set the API URL")),console.log("")}var h=new de;h.name("shipscanner").description("The credit score for AI-generated code. Scan any GitHub repo in seconds.").version("0.1.0");h.command("scan").description("Scan a GitHub repository and get its quality score (300-850)").argument("<url>","GitHub repo URL or owner/repo shorthand").option("-b, --branch <branch>","Branch to scan (defaults to repo default)").option("-j, --json","Output raw JSON (for agents and CI)").option("-t, --threshold <score>","Fail if score is below this (300-850)").option("--no-diff","Skip fetching score delta from previous scan").action(z);h.command("status").description("Check the status of a scan by ID").argument("<scan-id>","Scan ID (returned by the scan command)").option("-j, --json","Output raw JSON").action(V);h.command("login").description("Authenticate with your ShipScanner API key").option("-k, --key <api-key>","API key (starts with sk_)").action(Y);h.command("logout").description("Remove stored API key").action(q);h.command("whoami").description("Show current authentication status").action(Z);h.command("config").description("View or update CLI configuration").option("--api-url <url>","Set the API URL (default: https://shipscanner.dev)").action(Q);h.parse();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "shipscanner",
3
+ "version": "0.1.0",
4
+ "description": "The credit score for AI-generated code. Scan any GitHub repo from your terminal.",
5
+ "type": "module",
6
+ "bin": {
7
+ "shipscanner": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup src/index.ts --format esm --clean --minify",
14
+ "dev": "tsup src/index.ts --format esm --watch",
15
+ "typecheck": "tsc --noEmit",
16
+ "prepublishOnly": "pnpm build"
17
+ },
18
+ "keywords": [
19
+ "shipscanner",
20
+ "code-quality",
21
+ "security",
22
+ "ai-code",
23
+ "vibe-coding",
24
+ "scanner",
25
+ "cli",
26
+ "github",
27
+ "mcp"
28
+ ],
29
+ "author": "Youssef <hello@shipscanner.dev>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/shipscanner/cli"
34
+ },
35
+ "homepage": "https://shipscanner.dev",
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^13.1.0",
41
+ "chalk": "^5.4.1",
42
+ "ora": "^8.2.0",
43
+ "cli-table3": "^0.6.5",
44
+ "open": "^10.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.0.0",
48
+ "tsup": "^8.4.0",
49
+ "typescript": "^5.5.0"
50
+ }
51
+ }