diff --git a/dist/index.js b/dist/index.js index 668faec..26eb977 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,4 +1,4 @@ #!/usr/bin/env node -import{Command as we}from"commander";var N={name:"@cycrilabs/ws-ctrl",version:"1.4.0",description:"CLI tool to initialize a development workspace",repository:{type:"git",url:"git+https://github.com/CycriLabs/ws-ctrl.git"},publishConfig:{access:"public"},author:"Marc Scheib",license:"MIT",bugs:{url:"https://github.com/CycriLabs/ws-ctrl/issues"},homepage:"https://github.com/CycriLabs/ws-ctrl#readme",files:["dist"],bin:{"@cycrilabs/ws-ctrl":"dist/index.js"},type:"module",engines:{node:">=20"},scripts:{dev:"tsx src/index.ts",lint:"eslint ./src",test:"vitest","test:coverage":"vitest run --coverage",build:"tsup","build:watch":"tsup --watch",release:"npx --yes -p @semantic-release/changelog -p @semantic-release/git semantic-release",prepare:"husky"},devDependencies:{"@commitlint/cli":"^19.8.0","@commitlint/config-conventional":"^19.8.0","@eslint/js":"^9.25.1","@types/prompts":"^2.4.9","@vitest/coverage-v8":"^3.1.2",eslint:"^9.25.1",globals:"^16.0.0",husky:"^9.1.7","lint-staged":"^15.5.1",memfs:"^4.17.0",prettier:"^3.5.3",tempy:"^3.1.0",tsup:"^8.4.0",tsx:"^4.19.3",typescript:"^5.8.3","typescript-eslint":"^8.31.0",vitest:"^3.1.2"},dependencies:{chalk:"^5.4.1",commander:"^13.1.0",conf:"^13.1.0",dotenv:"^16.5.0",prompts:"^2.4.2"}};import{Command as ce,Option as pe}from"commander";import Ht from"conf";import jt from"chalk";var{red:it,yellow:nt,green:at,bold:E}=jt;function v(e){if(typeof e=="string")return e;if(Array.isArray(e))return`[${e.map(v).join(", ")}]`;if(e==null)return""+e;let t=e.overriddenName||e.name;if(t)return`${t}`;let r=e.toString();if(r==null)return""+r;let o=r.indexOf(` -`);return o>=0?r.slice(0,o):r}var $=class{constructor(t,r){this._desc=t}toString(){return`InjectionToken ${this._desc}`}},l=class{};function X(e){return typeof e=="function"}function ct(e){return!!(e&&e.useFactory)}var M=class extends l{records=new Map;constructor(t){super(),_t(t,r=>this.processProvider(r)),this.records.set(l,pt(void 0,this))}processProvider(t){let r=X(t)?t:t&&t.provide,o=Wt(t);if(this.records.get(r))throw new Error(`Token ${v(r)} already registered.`);this.records.set(r,o)}register(t){this.processProvider(t)}get(t){if(!this.records.has(t))throw new Error(`Could not find the token ${v(t)}`);let r=this.records.get(t);return r.value||(r.value=r.factory()),r.value}};function $t(e){if(X(e))return()=>new e;if(ct(e))return()=>e.useFactory();throw new Error(`Invalid provider: ${v(e)}`)}function Wt(e){let t=$t(e);return pt(t,void 0)}function pt(e,t){return{factory:e,value:t}}function _t(e,t){for(let r of e)t(r)}var Q;function mt(){return Q}function Y(e){let t=Q;return Q=e,t}function n(e){let t=mt();if(t===void 0)throw new Error(`The \`${v(e)}\` token injection failed because the injector is not set.`);return t.get(e)}function Z(e=null){return new M(e||[])}import{promises as I}from"node:fs";import{extname as Nt,join as tt}from"node:path";async function S(e){return I.readFile(e,"utf8")}async function L(e,t){return I.writeFile(e,t,"utf8")}async function W(e,t,r=[]){try{await I.mkdir(t,{recursive:!0})}catch(s){if(s instanceof Error&&"code"in s&&s.code!=="EEXIST")throw s}let o=await I.readdir(e);await Promise.all(o.filter(s=>!r.includes(s)).map(async s=>{let i=tt(e,s),a=tt(t,s);(await I.stat(i)).isDirectory()?await W(i,a):await I.copyFile(i,a)}))}async function A(e){try{let o=(await I.readdir(e)).filter(i=>Nt(i).toLowerCase()===".json").map(async i=>{let a=tt(e,i),c=await S(a);try{return JSON.parse(c)}catch(p){throw new Error(`Failed to parse JSON in file ${i}: ${p.message}`)}});return await Promise.all(o)}catch(t){throw new Error(`Error loading JSON files: ${t.message}`)}}import{spawnSync as ut}from"node:child_process";import{existsSync as Mt}from"node:fs";import{join as Lt}from"node:path";async function lt(e,t,r){return Mt(Lt(r,".git"))?ut("git",["pull"],{cwd:r,stdio:"inherit"}):ut("git",["clone",e,r],{cwd:t,stdio:"inherit"}),Promise.resolve()}var u=class{log(t,r){let o=r?r(t):t;console.log(o)}error(t){this.log(t,it)}warn(t){this.log(t,nt)}success(t){this.log(t,at)}};import{execSync as Bt}from"node:child_process";var ft=process.platform;async function B(e,t){return Bt(e,{cwd:t,stdio:"inherit"})}import{createHash as Gt}from"node:crypto";import{access as gt,constants as Kt}from"node:fs/promises";import{join as zt,resolve as Vt}from"node:path";function R(e){return Vt(e)}function et(e){let t=R(e);return Gt("md5").update(t).digest("hex").substring(0,10)}async function G(e){try{await gt(e)}catch{return n(u).error('The provided workspacePath "'+e+'" does not exist.'),!1}let t=et(e);try{return await gt(zt(e,`${t}.json`),Kt.F_OK),!0}catch{return n(u).error('Expected to find the file "'+t+'.json" but was not found.'),!1}}var y=new $("Config"),Jt={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},k;function dt(e){return k=new Ht({schema:Jt,cwd:R(e),configName:et(e)}),k}function qt(e){return{workspacePath:R(e),organization:"none",templatesRepository:null}}function xt(e,t,r){return k=dt(e),k.set("workspacePath",R(e)),k.set("organization",t),k.set("templatesRepository",r),k}async function K(e,t=!1){let r=e.trim();if(t)return n(u).log("Running within non-workspace directory..."),qt(r);if(!await G(r))throw new Error("The given path is no valid workspace.");return dt(r).store}import{join as x}from"node:path";var yt="templates",h="config",ht="docker",wt="git-templates";var Ct="repositories",Pt="servers",Tt="use-cases",rt="development";import{dirname as Xt}from"node:path";import{fileURLToPath as Qt}from"node:url";function Et(){let e=Qt(import.meta.url);return Xt(e)}var m=class{logger=n(u);config=n(y);getPackageTemplatesDir(){return x(this.#t(1),yt)}getGitTemplatesDir(){return x(this.#t(),h,wt)}getConfigDir(){return x(this.#t(),h)}getDockerDir(){return x(this.#t(),h,ht)}getServersDir(){return x(this.#t(),h,Pt)}getUseCasesDir(){return x(this.#t(),h,Tt)}getRepositoriesDir(){return x(this.#t(),h,Ct)}getDevelopmentDir(){return x(this.#t(),h,rt)}getWorkingDir(){return x(this.#t(),rt)}#t(t=0){return t===0?this.getWorkspacePath():Et()}getWorkspacePath(){return this.config.workspacePath}getTemplatesRepository(){return this.config.templatesRepository}createRepositoryUrl(t){return`git@bitbucket.org:${this.config.organization}/${t}.git`}async initWorkspace(){await this.syncTemplates(),await this.copyDevelopmentDirectory()}async syncTemplates(){let t=this.getPackageTemplatesDir();this.logger.log(`Syncing templates from ${t}...`),await W(t,this.getWorkspacePath()),await this.syncRepositoryTemplates()}async syncRepositoryTemplates(){let t=this.getTemplatesRepository();if(t){let r=this.createRepositoryUrl(t);this.logger.log(`Syncing templates from repository ${r}...`),await lt(r,this.getWorkspacePath(),this.getGitTemplatesDir()),await W(this.getGitTemplatesDir(),this.getConfigDir(),[".git"])}}async copyDevelopmentDirectory(){await W(this.getDevelopmentDir(),this.getWorkingDir())}};var w=class{templatesAccess=n(m);async loadUseCases(...t){let r=["DISABLED",...t];return this.loadFiles().then(o=>o.map(s=>this.#t(s))).then(o=>o.filter(s=>!r.includes(s.state)))}async loadFiles(){return A(this.templatesAccess.getUseCasesDir())}#t(t){return{...t,description:t.description||"",state:t.state||"ENABLED"}}};var F=class{executeFormula(t,r){try{let o=this.#t(t,r);return(0,eval)(o)}catch(o){throw new Error(`Error executing formula: ${o}`)}}#t(t,r){return`${Object.entries(r).reduce((s,[i,a])=>`${s}const ${i} = ${JSON.stringify(a)};`,"")}${t}`}};var U=class{templatesAccess=n(m);async loadRepositories(){return this.loadFiles().then(t=>t.map(r=>this.#t(r)))}async loadFiles(){return A(this.templatesAccess.getRepositoriesDir())}#t(t){return{...t,alias:t.alias||t.name,url:t.url||this.templatesAccess.createRepositoryUrl(t.name),attributes:{type:"UNKNOWN",...t.attributes}}}};var b=class{templatesAccess=n(m);async loadServers(){return this.loadFiles()}async loadFiles(){return A(this.templatesAccess.getServersDir())}};var O=class{templatesAccess=n(m);serversRepository=n(b);repositoriesRepository=n(U);async createContext(t={}){return{...t,WORKSPACE_PATH:this.templatesAccess.getWorkspacePath(),WORKING_DIR:this.templatesAccess.getWorkingDir(),SERVERS:await this.serversRepository.loadServers(),REPOSITORIES:await this.repositoriesRepository.loadRepositories(),OS:ft}}};var C=class{constructor(t,r){this.scriptExecutor=t;this.templatesAccess=r}async execute(t,r){throw new Error("Method not implemented.")}};var z=class extends C{async execute(t,r){let{inputFile:o,outputFile:s,context:i}=t;if(!o)throw new Error("No input file provided.");if(!i?.name||!i?.value)throw new Error("Missing replacement target or value.");let a=this.scriptExecutor.executeFormula(o,r),c=await S(a),{name:p,value:d}=i,g=this.scriptExecutor.executeFormula(p,r).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),T=this.scriptExecutor.executeFormula(d,r),j=c.replace(new RegExp(`${g}=.*`),`${g}=${T}`),q=this.scriptExecutor.executeFormula(s||o,r);return await L(q,j),r}};import oe from"prompts";import Yt from"prompts";function D(e){e.aborted&&(process.stdout.write("\x1B[?25h"),process.stdout.write(` -`),process.exit(1))}function P(e){return typeof e=="string"&&!!e.trim()||typeof e=="boolean"}async function Zt(e,t,r,o={}){return Yt({onState:D,type:e,name:"entity",message:`Select the ${t}`,choices:r.filter(o.displayFilter||(()=>!0)).map(s=>({title:o.createTitle?o.createTitle(s):s.name,value:s.id}))}).then(({entity:s})=>s)}function te(e,t){let r=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof e=="string"){let o=r[e.trim()];if(o)return[o]}else{if(Array.isArray(e))return e.map(o=>r[o.trim()]);throw new Error(`Can find entity for value ${e}.`)}throw new Error(`Entity for selection ${E(e)} not found.`)}async function ee(e,t,r,o,s={}){let i=P(r)&&typeof r=="string"?r:await Zt(e,t,o,s);return te(i,o)}async function vt(e,t,r,o={}){let[s]=await ee("select",e,t,r,o);return s}import re from"prompts";async function It(e,t){return re({onState:D,type:e,name:"input",message:t,initial:e==="toggle"?!0:void 0,active:"yes",inactive:"no"}).then(({input:r})=>r)}async function _(e,t,r){return P(r)?r:It(e,t)}async function ot(e,t){let r=P(t)?t:await It("text",e);if(P(r)&&typeof r=="string")return r.trim();throw Error("No input path provided.")}async function se(e,t,r){return oe({onState:D,type:e,name:"input",message:t,choices:r.map(({id:o,name:s})=>({title:s,value:o}))}).then(({input:o})=>o)}function ie(e,t){let r=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof e=="string"){let o=r[e.trim()];if(o)return o}else{if(Array.isArray(e))return e.map(o=>r[o.trim()]);throw new Error(`Can find entity for value ${e}.`)}throw new Error(`Entity for selection ${E(e)} not found.`)}var V=class extends C{async execute(t,r){let{name:o,message:s,type:i,entities:a,entityKey:c}=t.context;if(!o||!s||!i)throw new Error("Prompt name, message or type missing.");let p;switch(i){case"select":case"multiselect":{let d=(c?r[c]:a)||[],g=await se(i,s,d);p=ie(g,d);break}default:{p=await _(i,s);break}}return{...r,[o]:p}}};import ne from"dotenv";var H=class extends C{async execute(t,r){let o={...r},s=`${this.templatesAccess.getWorkspacePath()}/services.env`,i=ne.parse(await S(s));Object.assign(o,i),Object.entries(t.context||{}).forEach(([q,Ot])=>{o[q]=this.scriptExecutor.executeFormula(Ot,o)});let{kcVersion:a,authServerUrl:c,authUser:p,authPassword:d,authTenant:g}=o,T=this.templatesAccess.getWorkspacePath(),j=`docker run --pull=always --env-file ${T}/.env --mount type=bind,src="${T}/config/secret-templates",target="/secret-templates,readonly" --mount type=bind,src="${T}/config/services-config",target="/output" --rm -i ghcr.io/cycrilabs/keycloak-configurator:${a} export-secrets -s ${c} -u ${p} -p "${d}" -r ${g} -c //secret-templates -o //output`;return await B(j,this.templatesAccess.getWorkspacePath()),o}};var st={"custom-prompt":V,"generate-service-configuration":H,"change-env-var-value":z};var f=class{logger=n(u);templatesAccess=n(m);useCasesRepository=n(w);contextCreator=n(O);scriptExecutor=n(F);async run(t,r={}){if(typeof t=="string"){let s=(await this.useCasesRepository.loadUseCases()).find(({id:i})=>i===t);if(!s)throw new Error(`Use case not found: ${t}`);return await this.#t(s,r)}else return await this.#t(t,r)}async#t(t,r={}){let{name:o,description:s,steps:i}=t,a=await this.contextCreator.createContext(r);return this.logger.log(`Running use case ${E(o)}...`),s&&this.logger.log(`${s}`),i&&i.length!==0?(this.logger.log("Executing steps..."),await this.#s(i,a)):a}async#s(t,r){let o={...r,STEPS:t};for(let s=0;s{let o=this.scriptExecutor.executeFormula(t.formula,r);return t.outputFile&&typeof o=="string"&&await L(t.outputFile,o),{...r,[t.resultVariable||"STEP_RESULT"]:o}})}async#a(t,r){if(!t.command)throw new Error("Command missing in step.");return await this.#o(t,r,async()=>{let o=this.scriptExecutor.executeFormula(t.command,r);return await B(o,this.templatesAccess.getWorkspacePath()),r})}async#c(t,r){if(!t.executor||!st[t.executor])throw new Error("Executor missing or not found: "+t.executor);return this.#r(t.executor).execute(t,r)}async#p(t,r){return this.#r("custom-prompt").execute(t,r)}#r(t){return new st[t](this.scriptExecutor,this.templatesAccess)}async#m(t,r){if(!t.useCase)throw new Error("Use case missing in step.");let s=(await this.useCasesRepository.loadUseCases("INITIAL")).find(i=>i.id===t.useCase);if(!s)throw new Error(`Use case not found: ${t.useCase}`);return this.logger.log("Starting to run references use case..."),await this.run(s,r)}async#o(t,r,o){try{return await o()}catch(s){if(t.catchErrors)return this.logger.error(`Error caught: ${s.message}. Continuing.`),r;throw s}}};import{Argument as ae}from"commander";var St=()=>new ae("[workspace-path]","path to the workspace"),Rt=St(),J=St().default(".");var me=new pe("--templates-repository ","use a template repository");async function ue(e){return P(e)&&typeof e=="string"?e.trim():await _("toggle","Do you want to use a template repository?")?await _("text","Enter the name of the template repository"):null}async function le(e,t){let r=await ot("Path to the target workspace directory",e),o=R(r);if(await G(o))throw new Error("A workspace for the given path is already existing.");let i=await ot("What is the name of your organization?"),a=await ue(t.templatesRepository),c=xt(o,i,a);n(l).register({provide:y,useFactory:()=>c.store}),await n(m).initWorkspace(),await n(f).run("init")}var kt=new ce().name("init").description("initialize the workspace").addArgument(Rt).addOption(me).action(le);import{Command as fe,Option as At}from"commander";var ge=new At("-u, --use-case [use-case]","execute the use case with the given name"),de=new At("--debug","enable debug mode");async function xe(e,t){let r=await K(e,t.debug);n(l).register({provide:y,useFactory:()=>r});let o=n(f),i=await n(w).loadUseCases("INITIAL"),a=await vt("Use case",t.useCase,i,{createTitle:c=>`${c.name} (${c.id})`,displayFilter:c=>c.state!=="HIDDEN"});await o.run(a)}var Ft=new fe().name("run").description("run a use case in the workspace").addArgument(J).addOption(ge).addOption(de).action(xe);import{Command as ye}from"commander";async function he(e){let t=await K(e);n(l).register({provide:y,useFactory:()=>t});let r=n(m),o=n(f);await r.syncTemplates(),await o.run("sync")}var Ut=new ye().name("sync").description("syncs the workspace templates with the package & configured repository").addArgument(J).action(he);var bt=()=>process.exit(0);process.on("SIGINT",bt);process.on("SIGTERM",bt);process.on("uncaughtException",e=>{n(u).error(e.message),process.exit(1)});var Ce=Z([u,F,m,U,b,w,O,f]);Y(Ce);new we().name(N.name).description(N.description).version(N.version).addCommand(kt).addCommand(Ft).addCommand(Ut).parseAsync().catch(e=>{n(u).error(e.message),process.exit(1)}).finally(()=>n(u).success("Finished execution.")); +import{Command as wt}from"commander";var M={name:"@cycrilabs/ws-ctrl",version:"1.4.0",description:"CLI tool to initialize a development workspace",repository:{type:"git",url:"git+https://github.com/CycriLabs/ws-ctrl.git"},publishConfig:{access:"public"},author:"Marc Scheib",license:"MIT",bugs:{url:"https://github.com/CycriLabs/ws-ctrl/issues"},homepage:"https://github.com/CycriLabs/ws-ctrl#readme",files:["dist"],bin:{"@cycrilabs/ws-ctrl":"dist/index.js"},type:"module",engines:{node:">=20"},scripts:{dev:"tsx src/index.ts",lint:"eslint ./src",test:"vitest","test:coverage":"vitest run --coverage",build:"tsup","build:watch":"tsup --watch",release:"npx --yes -p @semantic-release/changelog -p @semantic-release/git semantic-release",prepare:"husky"},devDependencies:{"@commitlint/cli":"^19.8.0","@commitlint/config-conventional":"^19.8.0","@eslint/js":"^9.25.1","@types/prompts":"^2.4.9","@vitest/coverage-v8":"^3.1.2",eslint:"^9.25.1",globals:"^16.0.0",husky:"^9.1.7","lint-staged":"^15.5.1",memfs:"^4.17.0",prettier:"^3.5.3",tempy:"^3.1.0",tsup:"^8.4.0",tsx:"^4.19.3",typescript:"^5.8.3","typescript-eslint":"^8.31.0",vitest:"^3.1.2"},dependencies:{chalk:"^5.4.1",commander:"^13.1.0",conf:"^13.1.0",dotenv:"^16.5.0",prompts:"^2.4.2"}};import{Command as ct,Option as pt}from"commander";import He from"conf";import je from"chalk";var{red:ie,yellow:ne,green:ae,bold:E}=je;function v(t){if(typeof t=="string")return t;if(Array.isArray(t))return`[${t.map(v).join(", ")}]`;if(t==null)return""+t;let e=t.overriddenName||t.name;if(e)return`${e}`;let r=t.toString();if(r==null)return""+r;let o=r.indexOf(` +`);return o>=0?r.slice(0,o):r}var $=class{constructor(e,r){this._desc=e}toString(){return`InjectionToken ${this._desc}`}},l=class{};function X(t){return typeof t=="function"}function ce(t){return!!(t&&t.useFactory)}var N=class extends l{records=new Map;constructor(e){super(),_e(e,r=>this.processProvider(r)),this.records.set(l,pe(void 0,this))}processProvider(e){let r=X(e)?e:e&&e.provide,o=We(e);if(this.records.get(r))throw new Error(`Token ${v(r)} already registered.`);this.records.set(r,o)}register(e){this.processProvider(e)}get(e){if(!this.records.has(e))throw new Error(`Could not find the token ${v(e)}`);let r=this.records.get(e);return r.value||(r.value=r.factory()),r.value}};function $e(t){if(X(t))return()=>new t;if(ce(t))return()=>t.useFactory();throw new Error(`Invalid provider: ${v(t)}`)}function We(t){let e=$e(t);return pe(e,void 0)}function pe(t,e){return{factory:t,value:e}}function _e(t,e){for(let r of t)e(r)}var Q;function me(){return Q}function Y(t){let e=Q;return Q=t,e}function n(t){let e=me();if(e===void 0)throw new Error(`The \`${v(t)}\` token injection failed because the injector is not set.`);return e.get(t)}function Z(t=null){return new N(t||[])}import{promises as I}from"node:fs";import{extname as Me,join as ee}from"node:path";async function S(t){return I.readFile(t,"utf8")}async function L(t,e){return I.writeFile(t,e,"utf8")}async function W(t,e,r=[]){try{await I.mkdir(e,{recursive:!0})}catch(s){if(s instanceof Error&&"code"in s&&s.code!=="EEXIST")throw s}let o=await I.readdir(t);await Promise.all(o.filter(s=>!r.includes(s)).map(async s=>{let i=ee(t,s),a=ee(e,s);(await I.stat(i)).isDirectory()?await W(i,a):await I.copyFile(i,a)}))}async function A(t){try{let o=(await I.readdir(t)).filter(i=>Me(i).toLowerCase()===".json").map(async i=>{let a=ee(t,i),c=await S(a);try{return JSON.parse(c)}catch(p){throw new Error(`Failed to parse JSON in file ${i}: ${p.message}`)}});return await Promise.all(o)}catch(e){throw new Error(`Error loading JSON files: ${e.message}`)}}import{spawnSync as ue}from"node:child_process";import{existsSync as Ne}from"node:fs";import{join as Le}from"node:path";async function le(t,e,r){return Ne(Le(r,".git"))?ue("git",["pull"],{cwd:r,stdio:"inherit"}):ue("git",["clone",t,r],{cwd:e,stdio:"inherit"}),Promise.resolve()}var u=class{log(e,r){let o=r?r(e):e;console.log(o)}error(e){this.log(e,ie)}warn(e){this.log(e,ne)}success(e){this.log(e,ae)}};import{execSync as Be}from"node:child_process";var ge=process.platform;async function B(t,e){return Be(t,{cwd:e,stdio:"inherit"})}import{createHash as Ge}from"node:crypto";import{access as fe,constants as Ke}from"node:fs/promises";import{join as ze,resolve as Ve}from"node:path";function R(t){return Ve(t)}function te(t){let e=R(t);return Ge("md5").update(e).digest("hex").substring(0,10)}async function G(t){try{await fe(t)}catch{return!1}let e=te(t);try{return await fe(ze(t,`${e}.json`),Ke.F_OK),!0}catch{return n(u).error('Expected to find the file "'+e+'.json" but was not found.'),!1}}var h=new $("Config"),Je={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},k;function de(t){return k=new He({schema:Je,cwd:R(t),configName:te(t)}),k}function qe(t){return{workspacePath:R(t),organization:"none",templatesRepository:null}}function xe(t,e,r){return k=de(t),k.set("workspacePath",R(t)),k.set("organization",e),k.set("templatesRepository",r),k}async function K(t,e=!1){let r=t.trim();if(e)return n(u).log("Running within non-workspace directory..."),qe(r);if(!await G(r))throw new Error("The given path is no valid workspace.");return de(r).store}import{join as x}from"node:path";var he="templates",y="config",ye="docker",we="git-templates";var Ce="repositories",Pe="servers",Te="use-cases",re="development";import{dirname as Xe}from"node:path";import{fileURLToPath as Qe}from"node:url";function Ee(){let t=Qe(import.meta.url);return Xe(t)}var m=class{logger=n(u);config=n(h);getPackageTemplatesDir(){return x(this.#e(1),he)}getGitTemplatesDir(){return x(this.#e(),y,we)}getConfigDir(){return x(this.#e(),y)}getDockerDir(){return x(this.#e(),y,ye)}getServersDir(){return x(this.#e(),y,Pe)}getUseCasesDir(){return x(this.#e(),y,Te)}getRepositoriesDir(){return x(this.#e(),y,Ce)}getDevelopmentDir(){return x(this.#e(),y,re)}getWorkingDir(){return x(this.#e(),re)}#e(e=0){return e===0?this.getWorkspacePath():Ee()}getWorkspacePath(){return this.config.workspacePath}getTemplatesRepository(){return this.config.templatesRepository}createRepositoryUrl(e){return`git@bitbucket.org:${this.config.organization}/${e}.git`}async initWorkspace(){await this.syncTemplates(),await this.copyDevelopmentDirectory()}async syncTemplates(){let e=this.getPackageTemplatesDir();this.logger.log(`Syncing templates from ${e}...`),await W(e,this.getWorkspacePath()),await this.syncRepositoryTemplates()}async syncRepositoryTemplates(){let e=this.getTemplatesRepository();if(e){let r=this.createRepositoryUrl(e);this.logger.log(`Syncing templates from repository ${r}...`),await le(r,this.getWorkspacePath(),this.getGitTemplatesDir()),await W(this.getGitTemplatesDir(),this.getConfigDir(),[".git"])}}async copyDevelopmentDirectory(){await W(this.getDevelopmentDir(),this.getWorkingDir())}};var w=class{templatesAccess=n(m);async loadUseCases(...e){let r=["DISABLED",...e];return this.loadFiles().then(o=>o.map(s=>this.#e(s))).then(o=>o.filter(s=>!r.includes(s.state)))}async loadFiles(){return A(this.templatesAccess.getUseCasesDir())}#e(e){return{...e,description:e.description||"",state:e.state||"ENABLED"}}};var F=class{executeFormula(e,r){try{let o=this.#e(e,r);return(0,eval)(o)}catch(o){throw new Error(`Error executing formula: ${o}`)}}#e(e,r){return`${Object.entries(r).reduce((s,[i,a])=>`${s}const ${i} = ${JSON.stringify(a)};`,"")}${e}`}};var U=class{templatesAccess=n(m);async loadRepositories(){return this.loadFiles().then(e=>e.map(r=>this.#e(r)))}async loadFiles(){return A(this.templatesAccess.getRepositoriesDir())}#e(e){return{...e,alias:e.alias||e.name,url:e.url||this.templatesAccess.createRepositoryUrl(e.name),attributes:{type:"UNKNOWN",...e.attributes}}}};var b=class{templatesAccess=n(m);async loadServers(){return this.loadFiles()}async loadFiles(){return A(this.templatesAccess.getServersDir())}};var O=class{templatesAccess=n(m);serversRepository=n(b);repositoriesRepository=n(U);async createContext(e={}){return{...e,WORKSPACE_PATH:this.templatesAccess.getWorkspacePath(),WORKING_DIR:this.templatesAccess.getWorkingDir(),SERVERS:await this.serversRepository.loadServers(),REPOSITORIES:await this.repositoriesRepository.loadRepositories(),OS:ge}}};var C=class{constructor(e,r){this.scriptExecutor=e;this.templatesAccess=r}async execute(e,r){throw new Error("Method not implemented.")}};var z=class extends C{async execute(e,r){let{inputFile:o,outputFile:s,context:i}=e;if(!o)throw new Error("No input file provided.");if(!i?.name||!i?.value)throw new Error("Missing replacement target or value.");let a=this.scriptExecutor.executeFormula(o,r),c=await S(a),{name:p,value:d}=i,f=this.scriptExecutor.executeFormula(p,r).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),T=this.scriptExecutor.executeFormula(d,r),j=c.replace(new RegExp(`${f}=.*`),`${f}=${T}`),q=this.scriptExecutor.executeFormula(s||o,r);return await L(q,j),r}};import ot from"prompts";import Ye from"prompts";function D(t){t.aborted&&(process.stdout.write("\x1B[?25h"),process.stdout.write(` +`),process.exit(1))}function P(t){return typeof t=="string"&&!!t.trim()||typeof t=="boolean"}async function Ze(t,e,r,o={}){return Ye({onState:D,type:t,name:"entity",message:`Select the ${e}`,choices:r.filter(o.displayFilter||(()=>!0)).map(s=>({title:o.createTitle?o.createTitle(s):s.name,value:s.id}))}).then(({entity:s})=>s)}function et(t,e){let r=e.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof t=="string"){let o=r[t.trim()];if(o)return[o]}else{if(Array.isArray(t))return t.map(o=>r[o.trim()]);throw new Error(`Can find entity for value ${t}.`)}throw new Error(`Entity for selection ${E(t)} not found.`)}async function tt(t,e,r,o,s={}){let i=P(r)&&typeof r=="string"?r:await Ze(t,e,o,s);return et(i,o)}async function ve(t,e,r,o={}){let[s]=await tt("select",t,e,r,o);return s}import rt from"prompts";async function Ie(t,e){return rt({onState:D,type:t,name:"input",message:e,initial:t==="toggle"?!0:void 0,active:"yes",inactive:"no"}).then(({input:r})=>r)}async function _(t,e,r){return P(r)?r:Ie(t,e)}async function oe(t,e){let r=P(e)?e:await Ie("text",t);if(P(r)&&typeof r=="string")return r.trim();throw Error("No input path provided.")}async function st(t,e,r){return ot({onState:D,type:t,name:"input",message:e,choices:r.map(({id:o,name:s})=>({title:s,value:o}))}).then(({input:o})=>o)}function it(t,e){let r=e.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof t=="string"){let o=r[t.trim()];if(o)return o}else{if(Array.isArray(t))return t.map(o=>r[o.trim()]);throw new Error(`Can find entity for value ${t}.`)}throw new Error(`Entity for selection ${E(t)} not found.`)}var V=class extends C{async execute(e,r){let{name:o,message:s,type:i,entities:a,entityKey:c}=e.context;if(!o||!s||!i)throw new Error("Prompt name, message or type missing.");let p;switch(i){case"select":case"multiselect":{let d=(c?r[c]:a)||[],f=await st(i,s,d);p=it(f,d);break}default:{p=await _(i,s);break}}return{...r,[o]:p}}};import nt from"dotenv";var H=class extends C{async execute(e,r){let o={...r},s=`${this.templatesAccess.getWorkspacePath()}/services.env`,i=nt.parse(await S(s));Object.assign(o,i),Object.entries(e.context||{}).forEach(([q,Oe])=>{o[q]=this.scriptExecutor.executeFormula(Oe,o)});let{kcVersion:a,authServerUrl:c,authUser:p,authPassword:d,authTenant:f}=o,T=this.templatesAccess.getWorkspacePath(),j=`docker run --pull=always --env-file ${T}/.env --mount type=bind,src="${T}/config/secret-templates",target="/secret-templates,readonly" --mount type=bind,src="${T}/config/services-config",target="/output" --rm -i ghcr.io/cycrilabs/keycloak-configurator:${a} export-secrets -s ${c} -u ${p} -p "${d}" -r ${f} -c //secret-templates -o //output`;return await B(j,this.templatesAccess.getWorkspacePath()),o}};var se={"custom-prompt":V,"generate-service-configuration":H,"change-env-var-value":z};var g=class{logger=n(u);templatesAccess=n(m);useCasesRepository=n(w);contextCreator=n(O);scriptExecutor=n(F);async run(e,r={}){if(typeof e=="string"){let s=(await this.useCasesRepository.loadUseCases()).find(({id:i})=>i===e);if(!s)throw new Error(`Use case not found: ${e}`);return await this.#e(s,r)}else return await this.#e(e,r)}async#e(e,r={}){let{name:o,description:s,steps:i}=e,a=await this.contextCreator.createContext(r);return this.logger.log(`Running use case ${E(o)}...`),s&&this.logger.log(`${s}`),i&&i.length!==0?(this.logger.log("Executing steps..."),await this.#s(i,a)):a}async#s(e,r){let o={...r,STEPS:e};for(let s=0;s{let o=this.scriptExecutor.executeFormula(e.formula,r);return e.outputFile&&typeof o=="string"&&await L(e.outputFile,o),{...r,[e.resultVariable||"STEP_RESULT"]:o}})}async#a(e,r){if(!e.command)throw new Error("Command missing in step.");return await this.#o(e,r,async()=>{let o=this.scriptExecutor.executeFormula(e.command,r);return await B(o,this.templatesAccess.getWorkspacePath()),r})}async#c(e,r){if(!e.executor||!se[e.executor])throw new Error("Executor missing or not found: "+e.executor);return this.#r(e.executor).execute(e,r)}async#p(e,r){return this.#r("custom-prompt").execute(e,r)}#r(e){return new se[e](this.scriptExecutor,this.templatesAccess)}async#m(e,r){if(!e.useCase)throw new Error("Use case missing in step.");let s=(await this.useCasesRepository.loadUseCases("INITIAL")).find(i=>i.id===e.useCase);if(!s)throw new Error(`Use case not found: ${e.useCase}`);return this.logger.log("Starting to run references use case..."),await this.run(s,r)}async#o(e,r,o){try{return await o()}catch(s){if(e.catchErrors)return this.logger.error(`Error caught: ${s.message}. Continuing.`),r;throw s}}};import{Argument as at}from"commander";var Se=()=>new at("[workspace-path]","path to the workspace"),Re=Se(),J=Se().default(".");var mt=new pt("--templates-repository ","use a template repository");async function ut(t){return P(t)&&typeof t=="string"?t.trim():await _("toggle","Do you want to use a template repository?")?await _("text","Enter the name of the template repository"):null}async function lt(t,e){let r=await oe("Path to the target workspace directory",t),o=R(r);if(await G(o))throw new Error("A workspace for the given path is already existing.");let i=await oe("What is the name of your organization?"),a=await ut(e.templatesRepository),c=xe(o,i,a);n(l).register({provide:h,useFactory:()=>c.store}),await n(m).initWorkspace(),await n(g).run("init")}var ke=new ct().name("init").description("initialize the workspace").addArgument(Re).addOption(mt).action(lt);import{Command as gt,Option as Ae}from"commander";var ft=new Ae("-u, --use-case [use-case]","execute the use case with the given name"),dt=new Ae("--debug","enable debug mode");async function xt(t,e){let r=await K(t,e.debug);n(l).register({provide:h,useFactory:()=>r});let o=n(g),i=await n(w).loadUseCases("INITIAL"),a=await ve("Use case",e.useCase,i,{createTitle:c=>`${c.name} (${c.id})`,displayFilter:c=>c.state!=="HIDDEN"});await o.run(a)}var Fe=new gt().name("run").description("run a use case in the workspace").addArgument(J).addOption(ft).addOption(dt).action(xt);import{Command as ht}from"commander";async function yt(t){let e=await K(t);n(l).register({provide:h,useFactory:()=>e});let r=n(m),o=n(g);await r.syncTemplates(),await o.run("sync")}var Ue=new ht().name("sync").description("syncs the workspace templates with the package & configured repository").addArgument(J).action(yt);var be=()=>process.exit(0);process.on("SIGINT",be);process.on("SIGTERM",be);process.on("uncaughtException",t=>{n(u).error(t.message),process.exit(1)});var Ct=Z([u,F,m,U,b,w,O,g]);Y(Ct);new wt().name(M.name).description(M.description).version(M.version).addCommand(ke).addCommand(Fe).addCommand(Ue).parseAsync().catch(t=>{n(u).error(t.message),process.exit(1)}).finally(()=>n(u).success("Finished execution.")); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index bd60308..91b1590 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,6 +1,11 @@ import { ChalkInstance } from 'chalk'; import { green, red, yellow } from './colors.js'; +interface LogMessage { + level: string; + message: string; +} + export class Logger { log(message: string, color?: ChalkInstance) { const logMessage = color ? color(message) : message; @@ -19,3 +24,43 @@ export class Logger { this.log(message, green); } } + +export class MemoryLogger extends Logger { + private messages: LogMessage[] = []; + + log(message: string) { + this.messages.push({ + level: 'log', + message, + }); + } + + error(message: string) { + this.messages.push({ + level: 'error', + message: message, + }); + } + + warn(message: string) { + this.messages.push({ + level: 'warn', + message: message, + }); + } + + success(message: string) { + this.messages.push({ + level: 'success', + message: message, + }); + } + + getMessages(): LogMessage[] { + return this.messages; + } + + clear() { + this.messages = []; + } +} diff --git a/src/utils/workspace.test.ts b/src/utils/workspace.test.ts new file mode 100644 index 0000000..6976346 --- /dev/null +++ b/src/utils/workspace.test.ts @@ -0,0 +1,96 @@ +import { access } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { TestBed } from './di/test-bed.js'; +import { Logger, MemoryLogger } from './logger.js'; +import { + getWorkspacePathIdentifier, + isExistingWorkspace, + resolveWorkspacePath, +} from './workspace.js'; + +// Mock dependencies +vi.mock('node:fs/promises'); +vi.mock('node:path'); + +describe('workspace', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(resolve).mockImplementation(path => `/absolute/path/${path}`); + vi.mocked(join).mockImplementation((...paths) => paths.join('/')); + + TestBed.configureTestingModule({ + providers: [ + { + provide: Logger, + useFactory: () => new MemoryLogger(), + }, + ], + }); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe('resolveWorkspacePath', () => { + test('should resolve workspace path to absolute path', () => { + const workspacePath = 'test/workspace'; + const resolvedPath = resolveWorkspacePath(workspacePath); + + expect(resolvedPath).toBe('/absolute/path/test/workspace'); + }); + }); + + describe('getWorkspacePathIdentifier', () => { + test('should return a shortened md5 hash of the absolute path', () => { + const workspacePath = 'test/workspace'; + const identifier = getWorkspacePathIdentifier(workspacePath); + + expect(identifier).toBe('5b8583bb0c'); + }); + }); + + describe('isExistingWorkspace', () => { + test('should return false if workspace directory does not exist', async () => { + vi.mocked(access).mockRejectedValueOnce(new Error('Directory not found')); + + const result = await isExistingWorkspace('test/workspace'); + + const logger = TestBed.inject(Logger) as MemoryLogger; + expect(result).toBe(false); + expect(logger.getMessages().length).toBe(0); + }); + + test('should return true if workspace exists with config file', async () => { + const result = await isExistingWorkspace('test/workspace'); + + expect(result).toBe(true); + }); + + test('should return false if workspace directory exists but config file is missing', async () => { + vi.mocked(access).mockImplementation(path => { + if (path === 'test/workspace') { + return Promise.resolve(); + } + + if (path.toString().includes('.json')) { + return Promise.reject(new Error('Config file not found')); + } + + // Default behavior for other paths + return Promise.resolve(); + }); + + const result = await isExistingWorkspace('test/workspace'); + + const logger = TestBed.inject(Logger) as MemoryLogger; + expect(result).toBe(false); + expect(logger.getMessages().length).toBe(1); + expect(logger.getMessages()[0].level).toBe('error'); + expect(logger.getMessages()[0].message).toBe( + 'Expected to find the file "5b8583bb0c.json" but was not found.' + ); + }); + }); +}); diff --git a/src/utils/workspace.ts b/src/utils/workspace.ts index 3395e12..ab199fa 100644 --- a/src/utils/workspace.ts +++ b/src/utils/workspace.ts @@ -39,9 +39,6 @@ export async function isExistingWorkspace(workspacePath: string) { try { await access(workspacePath); } catch { - inject(Logger).error( - 'The provided workspacePath "' + workspacePath + '" does not exist.' - ); return false; }