diff --git a/README.md b/README.md index d8e4ad8..1b88352 100644 --- a/README.md +++ b/README.md @@ -191,14 +191,21 @@ This [JSON Schema](https://raw.githubusercontent.com/CycriLabs/ws-ctrl/refs/head ### Secret templates -The secret templates are used to configure the secrets that are necessary for all services -running locally to connect to the remote Keycloak and other services. In addition, -they container environment variables specific to each service. The secret templates +The secret templates are used to provide the secrets that are necessary for all services +running locally to connect to Keycloak or other services. In addition, +they can contain environment variables specific to each service. The secret templates are located in the `config/secret-templates` directory. The secret templates are based on the [Keycloak Configurator](https://github.com/CycriLabs/keycloak-configurator). For more details, check the [documentation](https://github.com/CycriLabs/keycloak-configurator?tab=readme-ov-file#sub-command-export-secrets). +If secret templates are the input for the `generate-service-configuration` executor, +then, the [Keycloak Configurator](https://github.com/CycriLabs/keycloak-configurator) gets +the `.env` file as input as well as additionally the following variables: + +- `KCC_WORKSPACE_PATH`: The absolute path to the workspace directory. +- `KCC_WORKING_DIR`: The absolute path to the working directory. + ## Use cases Use cases are scripts based on JSON that can be executed in the workspace. They diff --git a/dist/index.js b/dist/index.js index 26eb977..1dd261b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,4 +1,4 @@ #!/usr/bin/env node -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.")); +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.1","@commitlint/config-conventional":"^19.8.1","@eslint/js":"^9.27.0","@types/prompts":"^2.4.9","@vitest/coverage-v8":"^3.1.3",eslint:"^9.27.0",globals:"^16.1.0",husky:"^9.1.7","lint-staged":"^16.0.0",memfs:"^4.17.2",prettier:"^3.5.3",tempy:"^3.1.0",tsup:"^8.5.0",tsx:"^4.19.4",typescript:"^5.8.3","typescript-eslint":"^8.32.1",vitest:"^3.1.3"},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"fs";import{extname as Me,join as ee}from"path";async function R(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 R(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"child_process";import{existsSync as Ne}from"fs";import{join as Le}from"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"child_process";var ge=process.platform;async function B(t,e){return Be(t,{cwd:e,stdio:"inherit"})}import{createHash as Ge}from"crypto";import{access as fe,constants as Ke}from"fs/promises";import{join as ze,resolve as Ve}from"path";function S(t){return Ve(t)}function te(t){let e=S(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 x=new $("Config"),Je={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},k;function de(t){return k=new He({schema:Je,cwd:S(t),configName:te(t)}),k}function qe(t){return{workspacePath:S(t),organization:"none",templatesRepository:null}}function he(t,e,r){return k=de(t),k.set("workspacePath",S(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 h}from"path";var xe="templates",y="config",ye="docker",we="git-templates";var Ce="repositories",Pe="servers",Te="use-cases",re="development";import{dirname as Xe}from"path";import{fileURLToPath as Qe}from"url";function Ee(){let t=Qe(import.meta.url);return Xe(t)}var m=class{logger=n(u);config=n(x);getPackageTemplatesDir(){return h(this.#e(1),xe)}getGitTemplatesDir(){return h(this.#e(),y,we)}getConfigDir(){return h(this.#e(),y)}getDockerDir(){return h(this.#e(),y,ye)}getServersDir(){return h(this.#e(),y,Pe)}getUseCasesDir(){return h(this.#e(),y,Te)}getRepositoriesDir(){return h(this.#e(),y,Ce)}getDevelopmentDir(){return h(this.#e(),y,re)}getWorkingDir(){return h(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 O=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 U=class{templatesAccess=n(m);async loadServers(){return this.loadFiles()}async loadFiles(){return A(this.templatesAccess.getServersDir())}};var D=class{templatesAccess=n(m);serversRepository=n(U);repositoriesRepository=n(O);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 R(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 b(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:b,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:b,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:b,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 R(s));Object.assign(o,i),Object.entries(e.context||{}).forEach(([q,De])=>{o[q]=this.scriptExecutor.executeFormula(De,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 --env KCC_WORKSPACE_PATH=${this.templatesAccess.getWorkspacePath()} --env KCC_WORKING_DIR=${this.templatesAccess.getWorkingDir()} --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(D);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 Re=()=>new at("[workspace-path]","path to the workspace"),Se=Re(),J=Re().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=S(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=he(o,i,a);n(l).register({provide:x,useFactory:()=>c.store}),await n(m).initWorkspace(),await n(g).run("init")}var ke=new ct().name("init").description("initialize the workspace").addArgument(Se).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 ht(t,e){let r=await K(t,e.debug);n(l).register({provide:x,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(ht);import{Command as xt}from"commander";async function yt(t){let e=await K(t);n(l).register({provide:x,useFactory:()=>e});let r=n(m),o=n(g);await r.syncTemplates(),await o.run("sync")}var Oe=new xt().name("sync").description("syncs the workspace templates with the package & configured repository").addArgument(J).action(yt);var Ue=()=>process.exit(0);process.on("SIGINT",Ue);process.on("SIGTERM",Ue);process.on("uncaughtException",t=>{n(u).error(t.message),process.exit(1)});var Ct=Z([u,F,m,O,U,w,D,g]);Y(Ct);new wt().name(M.name).description(M.description).version(M.version).addCommand(ke).addCommand(Fe).addCommand(Oe).parseAsync().catch(t=>{n(u).error(t.message),process.exit(1)}).finally(()=>n(u).success("Finished execution.")); diff --git a/src/services/use-cases/executors/generate-service-configuration.ts b/src/services/use-cases/executors/generate-service-configuration.ts index bb67ed0..9fe6931 100644 --- a/src/services/use-cases/executors/generate-service-configuration.ts +++ b/src/services/use-cases/executors/generate-service-configuration.ts @@ -27,6 +27,8 @@ export class GenerateServiceConfiguration extends Executor { const command = `docker run \ --pull=always \ --env-file ${pwd}/.env \ + --env KCC_WORKSPACE_PATH=${this.templatesAccess.getWorkspacePath()} \ + --env KCC_WORKING_DIR=${this.templatesAccess.getWorkingDir()} \ --mount type=bind,src="${pwd}/config/secret-templates",target="/secret-templates,readonly" \ --mount type=bind,src="${pwd}/config/services-config",target="/output" \ --rm -i ghcr.io/cycrilabs/keycloak-configurator:${kcVersion} export-secrets -s ${authServerUrl} -u ${authUser} -p "${authPassword}" -r ${authTenant} -c //secret-templates -o //output`;