ssm-cli
Version:
Skydev serect management CLI
68 lines (61 loc) • 19.4 kB
JavaScript
var A="REPO_NAME",ae="REPO_LINK",D="REPO_ID",b="WORKING_DIR",ce="https://web-ssm.skydev.vn",pe="https://api-ssm.skydev.vn",$="ssm-cli",m={TRACKING_DATA:"tracking-data.json",ENV_VAULT:".env.vault",ENV_VERSION:".env.version",PAT:"pat.enc"},le="https://web-ssm.skydev.vn/generate-private-key";import N from"fs";import ze from"os";import R from"path";import Ye from"fs";import de from"path";import Me from"readline";var Ue=(e,t)=>{t===1&&(process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.setEncoding("utf8")),process.stdin.on("data",o=>{o==="q"&&(G(),process.exit())}),e.on("SIGINT",()=>{G(),e.close()})},H=async e=>{process.stdout.write(":"),await new Promise(t=>{e.once("line",()=>{fe(),t()})})},Be=async e=>{try{let{oneline:t}=e,o=t?20:4,n=I();if(!n)return;let{environment:r}=v(e),s=Me.createInterface({input:process.stdin,output:process.stdout}),a=1,c=0,l=!0,f=w(r);for(Ue(s,a);l;){let{data:h}=await E.getEnvLogs({page:a,take:o,pathWithNamespace:n[A],environment:r}),T=h.data;l=T.length===o;for(let Y of T)await me({oneline:t,take:o,logsShown:c,currentEnvVersion:f},Y,s),c++;if(l)a++;else break;c>=o&&await H(s)}s.close()}catch(t){let{message:o=""}=t;i.error(o),process.exit(1)}},J=Be;import y from"chalk";import ge from"readline";import{promisify as je}from"util";var We=je(setTimeout),i={info:(...e)=>{console.log(y.cyan(...e))},warn:(...e)=>{console.log(y.yellow(...e))},succeed:(...e)=>{console.log(y.green(...e))},error:(...e)=>{console.log(y.red(...e))},log:(...e)=>{console.log(...e)}},ue=(e,t)=>t===e?` (${y.cyan("HEAD")})`:"",me=async(e,t,o)=>{let{oneline:n,take:r,logsShown:s,currentEnvVersion:a}=e,{version:c,timestamp:l,commitMessage:f}=t,h=s>=r;n?await _(`${c} ${f}`,y.yellow,h,ue(a,c)):(await _(`commit ${c}`,y.yellow,h,ue(a,c)),await _(` Author: ${l.createdBy.username} <${l.createdBy.email}>`,y.white,h),await _(` Date: ${new Date(l.createdAt)}`,y.white,h),await _(` Message: ${f}`,y.white,h)),s>=r&&await H(o)},G=()=>{ge.cursorTo(process.stdout,0),ge.clearLine(process.stdout,0)},fe=()=>{process.stdout.write("\x1B[F"),G()},_=async(e,t=y.white,o=!1,n="")=>{for(let r of e)process.stdout.write(t(r)),o&&await We(8);process.stdout.write(n),process.stdout.write(`
`)};var u=(e="config.json")=>{let t=de.join(x(),e);if(!O(t))return{};let o=Ye.readFileSync(de.join(x(),e),"utf8");return JSON.parse(o.length===0||o===`
`?"{}":o)},C=()=>{let e=u();return e?.privateKey||(i.warn(`Encryption key is not set. Please set it using 'ssm-cli config set --private-key <key>'. Get key from ${le}`),process.exit(1)),e.privateKey};import*as p from"crypto";async function z(){let e=await p.webcrypto.subtle.generateKey({name:"RSA-OAEP",modulusLength:2048,publicExponent:new Uint8Array([1,0,1]),hash:"SHA-256"},!0,["encrypt","decrypt"]),t=await p.subtle.exportKey("spki",e.publicKey),o=Buffer.from(t).toString("base64");return{privateKey:e.privateKey,publicKey:o}}async function M(e){let t=p.getRandomValues(new Uint8Array(32)),o=p.getRandomValues(new Uint8Array(16)),n=await p.subtle.importKey("raw",t,{name:"AES-CBC"},!1,["encrypt"]),r=await p.subtle.encrypt({name:"AES-CBC",iv:o},n,new TextEncoder().encode(e));return{encryptedData:Buffer.from(r).toString("hex"),iv:Buffer.from(o).toString("hex"),clientKey:Buffer.from(t).toString("hex")}}async function k(e,t,o){try{let n=new Uint8Array(Buffer.from(e,"hex")).buffer,r=new Uint8Array(Buffer.from(t,"hex")),s=new Uint8Array(Buffer.from(o,"hex")),a=await p.subtle.importKey("raw",s,{name:"AES-CBC"},!1,["decrypt"]),c=await p.subtle.decrypt({name:"AES-CBC",iv:r},a,n);return new TextDecoder().decode(c)}catch(n){return n}}async function ye(e,t){let o=Uint8Array.from(atob(t),s=>s.charCodeAt(0)).buffer,n=await p.subtle.importKey("spki",o,{name:"RSA-OAEP",hash:"SHA-256"},!0,["encrypt"]),r=await p.subtle.encrypt({name:"RSA-OAEP"},n,new TextEncoder().encode(e));return Buffer.from(r).toString("hex")}async function q(e,t){let o=await p.subtle.decrypt({name:"RSA-OAEP"},t,new Uint8Array(Buffer.from(e,"hex")));return new TextDecoder().decode(o)}var ve=e=>{let t=C(),o=p.scryptSync(t,"salt",32),n=p.randomBytes(16),r=p.createCipheriv("aes-256-cbc",o,n),s=Buffer.concat([r.update(e,"utf8"),r.final()]);return n.toString("hex")+":"+s.toString("hex")},Ee=e=>{let t=C(),o=p.scryptSync(t,"salt",32),[n,r]=e.split(":");if(!n||!r)throw new Error("Invalid encrypted data format");let s=Buffer.from(n,"hex"),a=Buffer.from(r,"hex"),c=p.createDecipheriv("aes-256-cbc",o,s);return Buffer.concat([c.update(a),c.final()]).toString("utf8")};var Q=e=>{let t=p.createHash("sha256");return t.update(e),t.digest("hex")};import He from"fs";import Je from"path";var he=(e,t,o=!1)=>u(m.TRACKING_DATA)[e]!==(o?Q(t):t),P=(e,t,o=!1)=>{let n=Je.join(x(),m.TRACKING_DATA),r=u(m.TRACKING_DATA);r[e]=o?Q(t):t,F(n),He.writeFileSync(n,JSON.stringify(r,null,2))};var x=()=>{let e=ze.homedir();switch(process.platform){case"win32":return R.join(e,"AppData","Roaming",$);case"darwin":return R.join(e,"Library","Application Support",$);case"linux":default:return R.join(e,".config",$)}},F=e=>{let t=R.dirname(e);N.existsSync(t)||N.mkdirSync(t,{recursive:!0})},Ie=async e=>{let t=x(),o=R.join(t,m.PAT??"");F(o);let n=await M(e),r=ve(JSON.stringify(n));N.writeFileSync(o,JSON.stringify(r),{mode:384});let s=C();P("privateKey",s,!1)},xe=async()=>{let e=x(),t=R.join(e,m.PAT??"");if(!N.existsSync(t))return null;let o=JSON.parse(N.readFileSync(t,"utf8")),n=JSON.parse(Ee(o)),{encryptedData:r,iv:s,clientKey:a}=n;return await k(r,s,a)};import qe from"axios";import{config as Qe}from"dotenv";Qe();var X=qe.create({baseURL:`${pe}/api`,headers:{"Content-Type":"application/json"},withCredentials:!0});X.interceptors.response.use(e=>e.data,async e=>{let t=e.response?.data;return Promise.reject(t)});X.interceptors.request.use(async e=>{try{if(e.url?.includes("cli/login-session"))return e;let t=await xe();return t&&(e.headers.Authorization=`Bearer ${t}`),e}catch{i.warn("May be you change private key for decrypting access token. Please re-login")}return e});var g=X;var Xe={createCLILoginSession:()=>g.post("/cli/login-session"),verify:e=>g.get(`/cli/login-session/${e}`)},U=Xe;import*as Se from"diff";import d from"fs";import Ze from"path";var L=e=>{let t=I();return Ze.resolve(t?.[b]??"",e)};var Ce=e=>{try{return d.readFileSync(e,"utf8").split(`
`)}catch(t){return console.error("Error reading .env file:",t),[]}},Ae=(e,t)=>{let o=Se.diffLines(e,t),n="",r=!1;for(let s=0;s<o.length;s++){let a=o[s];if(a.removed||a.added){if(s==o.length-1)n+=a.value;else if(s<o.length-1){let c=o[s+1];if(c.added!=a.added&&c.removed!=a.removed){let l=we(a.value),f=we(c.value);c.value.startsWith(a.value)?n+=c.value:(r=!0,n+=`${l?"":`
`}<<<<<<< LOCAL
`,n+=a.value,n+=`${l?"":`
`}=======
`,n+=c.value,n+=`${f?"":`
`}>>>>>>> REMOTE
`),s++}else!c.added&&!c.removed&&(n+=a.value)}}else n+=a.value}return{mergedContent:n,hasConflicts:r}},we=e=>e.slice(-1)===`
`,B=({data:e,fileName:t})=>{let o;typeof e=="string"?o=e:o=Object.entries(e).map(([n,r])=>`${n}=${r}`).join(`
`),d.writeFile(t,o,n=>{n&&i.error(`Error creating ${t} file:`,n)})},K=({version:e,environment:t,fileName:o=m.ENV_VERSION??""})=>{let n=`ENV_${t.toUpperCase()}_VERSION`;try{let s=(Z()?d.readFileSync(o,"utf8"):"").split(`
`);s[s.length-1]===""&&s.pop();let a=!1,c=s.map(f=>f.startsWith(n)?(a=!0,`${n}=${e}`):f);a||c.push(`${n}=${e}`);let l=c.join(`
`);d.writeFile(o,l,f=>{f&&i.error(`Error updating ENV_VERSION in ${o}:`,f)})}catch(r){i.error(`Error updating ENV_VERSION in ${o}:`,r)}},w=(e,t=m.ENV_VERSION??"")=>d.readFileSync(t,"utf8").split(`
`).find(r=>r.startsWith(`ENV_${e.toUpperCase()}_VERSION=`))?.split("=")?.[1]??"",Z=(e=m.ENV_VERSION??"")=>d.existsSync(e),O=e=>d.existsSync(e);var Pe=(e=".env")=>{try{return d.readFileSync(e,"utf8")}catch{return i.warn(`No ${e} file found.`),""}},I=(e=m.ENV_VAULT??"")=>{try{let t=d.readFileSync(e,"utf8"),o={};return t.split(`
`).forEach(n=>{let[r,s]=n.split("=");o[r]=s}),o}catch{return i.warn("No repository found. Please run `ssm-cli init` first"),null}},Re=async(e,t=300,o=2e3)=>{for(let n=0;n<t;n++){try{let{data:{accessToken:r}}=await U.verify(e);return r}catch{}await new Promise(r=>setTimeout(r,o))}throw new Error("Login timed out. Please try again.")},Le=async(e=".gitignore")=>{let t=`
# Env files
.env*
!.env.project
!.env.vault
`;try{let o=d.readFileSync(e,"utf8");o.includes("# Env files")||(o+=`
`+t.trim()+`
`,d.writeFileSync(e,o),i.succeed(`${e} file updated with new rules`))}catch(o){o.code==="ENOENT"?(d.writeFileSync(e,t.trim()+`
`),i.succeed(`${e} file created with new rules`)):i.error(`Error updating ${e} file:`,o)}};var v=({production:e,develop:t,cicd:o,staging:n})=>e?{environment:"Production",fileName:".env.production"}:t?{environment:"Development",fileName:".env"}:o?{environment:"CI/CD",fileName:".env.cicd"}:n?{environment:"Staging",fileName:".env.staging"}:{environment:"Development",fileName:".env"};import et from"fs";import Ke from"path";import{fileURLToPath as tt}from"url";var ot=Ke.dirname(tt(import.meta.url)),_e={getContent(){let e=Ke.resolve(ot,"../","package.json");return JSON.parse(et.readFileSync(e,"utf-8"))},get version(){let e=this.getContent(),{version:t}=e;return t||"0.0.0"}};import nt from"figlet";var Oe=()=>{let e=nt.textSync("SSM CLI",{font:"Small"});i.info(`
${e}
`)};import{execSync as rt}from"child_process";var j=()=>{let e=rt("git config --get remote.origin.url").toString().trim();e||(i.error(`You are not in a git repository:
1. Check if Git origin is added to your project.
2. Your project must be pushed to the GITLAB remote repository.`),process.exit(1));let t="",o="",n="";if(e.includes("https://")){let r=e.split("/"),s=r.length;t=r[s-2],o=r[s-1].split(".git")[0],n=`${t}/${o}`}else n=e.split(":")[1].split(".git")[0],t=n.split("/")[0],o=n.split("/")[1];return{namespace:t,path:o,pathWithNamespace:n,origin:e}};var st={getLatestEnv:async e=>{let{publicKey:t,privateKey:o}=await z(),n=await g.get("key-values/latest",{params:{...e,publicKey:t}}),{data:r}=n;if(!r?.aesKey||!r?.encryptedKeyValues)return{decryptedData:null,version:""};let s=await q(r?.aesKey,o);return{decryptedData:await k(r?.encryptedKeyValues,s.slice(0,32),s.slice(33)),version:r.version}},getEnvByIdOrVersion:async e=>{let{publicKey:t,privateKey:o}=await z(),n=await g.get(`key-values/${e.idOrVersion}`,{params:{...e,publicKey:t}}),{data:r}=n;if(!r?.aesKey||!r?.encryptedKeyValues)return null;let s=await q(r?.aesKey,o);return await k(r?.encryptedKeyValues,s.slice(0,32),s.slice(33))},createEnv:async e=>{let{env:t,environment:o,repositoryId:n,commitMessage:r}=e,{statusCode:s,data:a}=await g.post("key-values",{environment:o,repositoryId:n});if(s===200){let{encryptedData:c,clientKey:l,iv:f}=await M(t),h=await ye([f,l].join(),a.publicKey),T={keys:c,aesKey:h,commitMessage:r},{data:Y}=await g.patch(`key-values/${a.id}`,T);K({version:Y.version,environment:o})}},updateEnv:(e,t)=>g.patch(`key-values/${e}`,t),getEnvLogs:e=>g.get("/key-values/histories",{params:e}),getTotalOfForwardVersions:e=>g.get("key-values/forwards",{params:e})},E=st;import ke from"fs";var it=async(e,t,o)=>{let{force:n}=o;if(n||!O(t))ke.writeFileSync(t,e,"utf-8");else{let r=Ce(t).join(`
`),{mergedContent:s,hasConflicts:a}=Ae(r,e);a&&i.warn("Conflicts detected in .env file. Please resolve them before pushing."),ke.writeFileSync(t,s,"utf-8")}},at=async e=>{try{let{environment:t,fileName:o}=v(e),n=I();if(!n)return;let{decryptedData:r,version:s}=await E.getLatestEnv({environment:t,pathWithNamespace:n[A]});if(Z()&&O(o)&&w(t)===s){i.log("Already up to date.");return}r&&(it(r,L(o),e),K({version:s,environment:t}),i.info(`.env file updated successfully${e?.force?" with force":""}.`))}catch(t){let o=t;i.error(o.message)}},ee=at;var ct=async e=>{let{message:t}=e;try{let{environment:o,fileName:n}=v(e),r=I();if(!r)return;let s=Pe(L(n));if(!s)return;he(o,s,!0)?(await E.createEnv({environment:o,repositoryId:r[D],env:s,commitMessage:t}),P(o,s,!0),i.info(`Push env ${o.toLowerCase()} successfully`)):i.warn(`Env ${o.toLowerCase()} is not changed`)}catch(o){let n=o;i.error(n.message)}},te=ct;var pt=async(e,t)=>{try{let{environment:o,fileName:n}=v(t);if(!I())return;if(w(o)===e){i.log("Already in this version.");return}let a=await E.getEnvByIdOrVersion({environment:o,idOrVersion:e});a&&(B({data:a,fileName:L(n)}),K({version:e,environment:o}),i.info("Revert env successfully"))}catch(o){let n=o;i.error(n.message)}},oe=pt;import lt from"chalk";var mt=async e=>{try{let{environment:t}=v(e),o=w(t),{data:n}=await E.getTotalOfForwardVersions({version:o,environment:t});i.log(`
Current ${t.toLowerCase()} version: `+lt.cyan(o)),n?.forwardVersionTotal>0&&(i.log(`Your version is behind by ${n?.forwardVersionTotal} commits, and can be fast-forwarded.
`),i.log('(use "ssm-cli pull" to update your local)")'))}catch(t){i.error(t.message)}},ne=mt;import ft from"chalk";import gt from"open";import Fe from"ora";var ut=async()=>{C();let e=Fe("Initiating login process...").start();try{let o=(await U.createCLILoginSession()).data.id??"",n=`${ce}/cli-sign-in?session-id=${o}`;i.info(`
Login URL: ${n}`),await gt(n),i.succeed("Login URL generated. Opening in your default browser..."),e.succeed(ft.yellow(" Please complete the login process in your browser."));let r=Fe("Waiting for login to complete...").start(),s=await Re(o);await Ie(s),i.succeed(`
Successfully logged in!`),r.succeed().stop(),Le()}catch(t){e.fail("Login process failed"),console.error("An error occurred during login:",t)}},re=ut;var dt={getRepo:async e=>g.get(`/repositories/${e}`),syncRepo:(e,t)=>g.post("/gitlab/repositories/sync-one",{namespace:e,path:t})},W=dt;import se from"chalk";import yt from"ora";var vt=async()=>{try{let{namespace:e,path:t,pathWithNamespace:o}=j(),n=yt("Initiating sync repository from Gitlab...").start();await W.syncRepo(e,t),n.succeed(` Repository ${se.green(o)} synced successfully`),console.log("You can now use this repository for managing secrets: "),console.log(` 1. Push ENV: ${se.green("ssm-cli push")}`),console.log(` 2. Pull ENV: ${se.green("ssm-cli pull")}`)}catch(e){let{statusCode:t}=e;t===401?i.warn("Your access token is invalid, please login again"):i.error("Error syncing repo:",e)}},V=vt;var Et=async(e,t,o)=>{let{sync:n}=o,r=t?.name,s=e;try{if(n&&await V(),!t?.name||t?.name?.includes(".")||t?.name?.includes("/")||t?.name?.includes("\\")){let{namespace:l,path:f}=j();r=`${l}-${f}`}let{data:{httpUrlToRepo:a,id:c}}=await W.getRepo(r);B({data:{[D]:c,[A]:r,[ae]:a,[b]:s},fileName:m.ENV_VAULT??""}),i.info("Repository initialized")}catch(a){let c=a;i.error(c.message),c.statusCode===401?i.warn("You do not have permission, please login first"):i.warn("Note: Please check if you have synchronized the repository: ssm-cli sync")}},ie=Et;import{Command as Ct}from"commander";import{config as At}from"dotenv";var Ne=()=>{let e=u();return i.info("Current Configurations:"),console.log(e),e};import ht from"fs";import Ve from"os";import Te from"path";var De=e=>{let t=process.env.SHELL,o=e.commands.map(s=>({command:"ssm-cli",subcommand:s.name(),suggestions:s.options.map(a=>a.flags)}));o.push({command:"ssm-cli",suggestions:e.commands.map(s=>s.name())});let n=`
# Autocomplete for ssm-cli
_ssm_cli_completion() {
local cur prev words cword
_init_completion || return
# Global suggestions
local global_suggestions="${o.find(s=>!s.subcommand)?.suggestions.join(" ")||""}"
# Suggestions for specific subcommands
declare -A subcommand_suggestions=(
${o.filter(s=>s.subcommand).map(s=>`["${s.subcommand}"]="${s.suggestions.join(" ")}"`).join(`
`)}
)
# First level completion (main commands)
if [ $cword -eq 1 ]; then
COMPREPLY=( $(compgen -W "$global_suggestions" -- "$cur") )
return 0
fi
# Subcommand-specific completions
local subcommand="\${words[1]}"
local subcommand_opts="\${subcommand_suggestions[$subcommand]}"
if [ -n "$subcommand_opts" ]; then
COMPREPLY=( $(compgen -W "$subcommand_opts" -- "$cur") )
return 0
fi
}
complete -F _ssm_cli_completion ssm-cli
`,r=t.includes("zsh")?Te.join(Ve.homedir(),".zshrc"):t.includes("bash")?Te.join(Ve.homedir(),".bashrc"):null;r?(ht.appendFileSync(r,n,{flag:"a"}),console.log(`Tab completion installed! Please restart your terminal or run:
source ${r}`)):(console.log("Unsupported shell. Please add the following script to your shell config manually:"),console.log(n))};import It from"fs";import xt from"path";var wt=e=>{let t=xt.join(x(),"config.json"),{privateKey:o}=e;if(o?.length>0){if(o.length!=64){i.error("Invalid encryption key. Key must be 64 characters long.");return}let n=u();n.privateKey=o,F(t),It.writeFileSync(t,JSON.stringify(n,null,2)),u(m.TRACKING_DATA)?.privateKey||P("privateKey",o,!1),i.succeed("Config set successfully. Change encryption key, you must login again.")}},be=e=>{wt(e)};var St=async()=>{let e=u(),t=u(m.TRACKING_DATA);e.privateKey&&t.privateKey&&e.privateKey!==t.privateKey&&(i.warn("Your private key has been changed. Please re-login"),process.exit(1))},S=e=>async(...t)=>{await St(),await e(...t)};At();(async()=>{Oe();let e=new Ct;e.version(_e.version,"-v, --version","display the version number").description("An CLI for managing projects").name("ssm-cli"),e.command("login").description("Login to the SSM").action(re);let t=e.command("config").description("Configurations");t.command("set").option("-pk, --private-key <key>","Set encryption key").description("Set up config for SSM").action(be),t.command("get").description("Get all configurations").action(Ne),e.command("sync").description("Synchronize the current repository (GIT) from Gitlab to SSM Registry").action(S(V)),e.command("pull").description("Pull Env").option("-d, --develop","Pull env develop (default)").option("-p, --production","Pull env production").option("-s, --staging","Pull env staging").option("-c, --cicd","Pull env ci cd").option("-f, --force","Force pull env").action(S(ee)),e.command("push").description("Push Env").requiredOption("-m, --message <message>","Commit message").option("-d, --develop","Push env develop (default)").option("-p, --production","Push env production").option("-s, --staging","Push env staging").option("-c, --cicd","Push env ci cd").action(S(te)),e.command("init").description("Initialize repository").option("-n, --name <repo-name>","Repository pathname").argument("<root-folder>","Root folder of the repository, must be in last position").option("--sync","Synchronize the current repository (GIT) from Gitlab to SSM Registry").action(S(ie)),e.command("log").option("--oneline","Get brief logs").option("-d, --develop","Head env develop (default)").option("-p, --production","Head env production").option("-s, --staging","Head env staging").option("-c, --cicd","Head env ci cd").description("Get logs").action(S(J)),e.command("revert").argument("<version>","Version of the env").option("-d, --develop","Head env develop (default)").option("-p, --production","Head env production").option("-s, --staging","Head env staging").option("-c, --cicd","Head env ci cd").description("Revert Env to a specific version of environment").action(S(oe)),e.command("head").option("-d, --develop","Head env develop (default)").option("-p, --production","Head env production").option("-s, --staging","Head env staging").option("-c, --cicd","Head env ci cd").description("Get the current version of ENV").action(S(ne)),e.command("auto-complete").description("Set up tab completion for your shell").action(()=>{De(e)}),e.parse(process.argv)})();