From b8c089a5e25804298ccbe6636e25058bf7eb32e7 Mon Sep 17 00:00:00 2001 From: Marc Scheib Date: Sat, 26 Apr 2025 13:19:13 +0200 Subject: [PATCH 1/2] feat(run): add debug mode to execute use cases on non-workspaces (#3) --- README.md | 17 ++++++++-- dist/index.js | 4 +-- src/commands/init/action.ts | 4 +-- src/commands/run/run.ts | 7 ++-- src/commands/sync/sync.ts | 2 +- src/config.ts | 41 ++++++++++++++++------- src/services/access/templates-access.ts | 19 +++++------ src/services/repositories.repository.ts | 15 +++------ src/services/use-cases/context-creator.ts | 5 ++- src/services/use-cases/use-case-runner.ts | 4 +-- 10 files changed, 68 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index db745dc..a91148d 100644 --- a/README.md +++ b/README.md @@ -91,9 +91,10 @@ the top-level workspace directory. The following options are available: -| Option | Description | -| --------------------------- | ------------------- | -| `-u, --use-case [use-case]` | the use case to run | +| Option | Description | +| --------------------------- | --------------------------------------------------------------------------------------------------- | +| `-u, --use-case [use-case]` | the use case to run | +| `--debug` | enable debug mode; allows to run use cases from any location, assuming the config structure matches | For example, to run the `check-requirements` use case, execute: @@ -395,3 +396,13 @@ After testing, the symlink can be removed via: ```bash npm uninstall ws-ctrl -g ``` + +### Use cases + +It is possible to test use cases directly in the workspace. To do so, the use case +should be placed in the `assets/templates/config/use-cases` directory. +The use case can then be executed via: + +```bash +npm run dev -- run ./assets/templates -u --debug +``` diff --git a/dist/index.js b/dist/index.js index e0c56ec..300e19f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,3 +1,3 @@ #!/usr/bin/env node -import{Command as ee}from"commander";var U={name:"@cycrilabs/ws-ctrl",version:"1.3.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 Vt,Option as Ht}from"commander";import{Argument as Pt}from"commander";var X=()=>new Pt("[workspace-path]","path to the workspace"),Q=X(),D=X().default(".");import Ft from"conf";import Tt from"chalk";var{red:Y,yellow:Z,green:tt,bold:C}=Tt;import{promises as w}from"node:fs";import{extname as St,join as K}from"node:path";async function E(r){return w.readFile(r,"utf8")}async function F(r,t){return w.writeFile(r,t,"utf8")}async function A(r,t,e=[]){try{await w.mkdir(t,{recursive:!0})}catch(s){if(s instanceof Error&&"code"in s&&s.code!=="EEXIST")throw s}let o=await w.readdir(r);await Promise.all(o.filter(s=>!e.includes(s)).map(async s=>{let i=K(r,s),a=K(t,s);(await w.stat(i)).isDirectory()?await A(i,a):await w.copyFile(i,a)}))}async function T(r){try{let o=(await w.readdir(r)).filter(i=>St(i).toLowerCase()===".json").map(async i=>{let a=K(r,i),c=await E(a);try{return JSON.parse(c)}catch(n){throw new Error(`Failed to parse JSON in file ${i}: ${n.message}`)}});return await Promise.all(o)}catch(t){throw new Error(`Error loading JSON files: ${t.message}`)}}import{spawnSync as et}from"node:child_process";import{existsSync as Rt}from"node:fs";import{join as kt}from"node:path";async function rt(r,t,e){return Rt(kt(e,".git"))?et("git",["pull"],{cwd:e,stdio:"inherit"}):et("git",["clone",r,e],{cwd:t,stdio:"inherit"}),Promise.resolve()}var z=class{log(t,e){let o=e?e(t):t;console.log(o)}error(t){this.log(t,Y)}warn(t){this.log(t,Z)}success(t){this.log(t,tt)}},p=new z;import{execSync as vt}from"node:child_process";async function O(r,t){return vt(r,{cwd:t,stdio:"inherit"})}import{createHash as At}from"node:crypto";import{access as ot,constants as It}from"node:fs/promises";import{join as Ut,resolve as Dt}from"node:path";function S(r){return Dt(r)}function V(r){let t=S(r);return At("md5").update(t).digest("hex").substring(0,10)}async function b(r){try{await ot(r)}catch{return p.error('The provided workspacePath "'+r+'" does not exist.'),!1}let t=V(r);try{return await ot(Ut(r,`${t}.json`),It.F_OK),!0}catch{return p.error('Expected to find the file "'+t+'.json" but was not found.'),!1}}var Ot={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},P;function st(r,t,e){return P=it(r),P.set("workspacePath",S(r)),P.set("organization",t),P.set("templatesRepository",e),P}function it(r){return P=new Ft({schema:Ot,cwd:S(r),configName:V(r)}),P}async function W(r){let t=r.trim();if(!await b(t))throw new Error("The given path is no valid workspace.");return it(t)}import{join as l}from"node:path";var nt="templates",x="config",at="docker",ct="git-templates";var pt="repositories",mt="servers",ut="use-cases",H="development";import{dirname as bt}from"node:path";import{fileURLToPath as Wt}from"node:url";function lt(){let r=Wt(import.meta.url);return bt(r)}var g=class r{constructor(t){this.config=t}static create(t){return new r(t)}getPackageTemplatesDir(){return l(this.#t(1),nt)}getGitTemplatesDir(){return l(this.#t(),x,ct)}getConfigDir(){return l(this.#t(),x)}getDockerDir(){return l(this.#t(),x,at)}getServersDir(){return l(this.#t(),x,mt)}getUseCasesDir(){return l(this.#t(),x,ut)}getRepositoriesDir(){return l(this.#t(),x,pt)}getDevelopmentDir(){return l(this.#t(),x,H)}getWorkingDir(){return l(this.#t(),H)}#t(t=0){return t===0?this.getWorkspacePath():lt()}getWorkspacePath(){return this.config.get("workspacePath")}getTemplatesRepository(){return this.config.get("templatesRepository")}createRepositoryUrl(t,e){return`git@bitbucket.org:${t}/${e}.git`}async initWorkspace(){await this.syncTemplates(),await this.copyDevelopmentDirectory()}async syncTemplates(){let t=this.getPackageTemplatesDir();p.log(`Syncing templates from ${t}...`),await A(t,this.getWorkspacePath()),await this.syncRepositoryTemplates()}async syncRepositoryTemplates(){let t=this.getTemplatesRepository();if(t){let e=this.createRepositoryUrl(this.config.get("organization"),t);p.log(`Syncing templates from repository ${e}...`),await rt(e,this.getWorkspacePath(),this.getGitTemplatesDir()),await A(this.getGitTemplatesDir(),this.getConfigDir(),[".git"])}}async copyDevelopmentDirectory(){await A(this.getDevelopmentDir(),this.getWorkingDir())}};var R=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadUseCases(...t){let e=["DISABLED",...t];return this.loadFiles().then(o=>o.map(s=>this.#t(s))).then(o=>o.filter(s=>!e.includes(s.state)))}async loadFiles(){return T(this.templatesAccess.getUseCasesDir())}#t(t){return{...t,description:t.description||"",state:t.state||"ENABLED"}}};var $=class r{static create(){return new r}executeFormula(t,e){try{let o=this.#t(t,e);return(0,eval)(o)}catch(o){throw new Error(`Error executing formula: ${o}`)}}#t(t,e){return`${Object.entries(e).reduce((s,[i,a])=>`${s}const ${i} = ${JSON.stringify(a)};`,"")}${t}`}};var N=class r{constructor(t,e){this.config=t;this.templatesAccess=e}static create(t,e){return new r(t,e)}async loadRepositories(){return this.loadFiles().then(t=>t.map(e=>this.#t(e)))}async loadFiles(){return T(this.templatesAccess.getRepositoriesDir())}#t(t){return{...t,alias:t.alias||t.name,url:t.url||this.templatesAccess.createRepositoryUrl(this.config.get("organization"),t.name),attributes:{type:"UNKNOWN",...t.attributes}}}};var _=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadServers(){return this.loadFiles()}async loadFiles(){return T(this.templatesAccess.getServersDir())}};var M=class r{constructor(t,e,o){this.templatesAccess=t;this.serversRepository=e;this.repositoriesRepository=o}static create(t,e){return new r(e,_.create(e),N.create(t,e))}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()}}};var y=class{constructor(t,e){this.scriptExecutor=t;this.templatesAccess=e}async execute(t,e){throw new Error("Method not implemented.")}};var B=class extends y{async execute(t,e){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,e),c=await E(a),{name:n,value:u}=i,m=this.scriptExecutor.executeFormula(n,e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),h=this.scriptExecutor.executeFormula(u,e),v=c.replace(new RegExp(`${m}=.*`),`${m}=${h}`),G=this.scriptExecutor.executeFormula(s||o,e);return await F(G,v),e}};import Lt from"prompts";import $t from"prompts";function k(r){r.aborted&&(process.stdout.write("\x1B[?25h"),process.stdout.write(` -`),process.exit(1))}function d(r){return typeof r=="string"&&!!r.trim()||typeof r=="boolean"}async function Nt(r,t,e,o={}){return $t({onState:k,type:r,name:"entity",message:`Select the ${t}`,choices:e.filter(o.displayFilter||(()=>!0)).map(s=>({title:o.createTitle?o.createTitle(s):s.name,value:s.id}))}).then(({entity:s})=>s)}function _t(r,t){let e=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof r=="string"){let o=e[r.trim()];if(o)return[o]}else{if(Array.isArray(r))return r.map(o=>e[o.trim()]);throw new Error(`Can find entity for value ${r}.`)}throw new Error(`Entity for selection ${C(r)} not found.`)}async function Mt(r,t,e,o,s={}){let i=d(e)&&typeof e=="string"?e:await Nt(r,t,o,s);return _t(i,o)}async function gt(r,t,e,o={}){let[s]=await Mt("select",r,t,e,o);return s}import Bt from"prompts";async function ft(r,t){return Bt({onState:k,type:r,name:"input",message:t,initial:r==="toggle"?!0:void 0,active:"yes",inactive:"no"}).then(({input:e})=>e)}async function I(r,t,e){return d(e)?e:ft(r,t)}async function J(r,t){let e=d(t)?t:await ft("text",r);if(d(e)&&typeof e=="string")return e.trim();throw Error("No input path provided.")}async function jt(r,t,e){return Lt({onState:k,type:r,name:"input",message:t,choices:e.map(({id:o,name:s})=>({title:s,value:o}))}).then(({input:o})=>o)}function Gt(r,t){let e=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof r=="string"){let o=e[r.trim()];if(o)return o}else{if(Array.isArray(r))return r.map(o=>e[o.trim()]);throw new Error(`Can find entity for value ${r}.`)}throw new Error(`Entity for selection ${C(r)} not found.`)}var L=class extends y{async execute(t,e){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 n;switch(i){case"select":case"multiselect":{let u=(c?e[c]:a)||[],m=await jt(i,s,u);n=Gt(m,u);break}default:{n=await I(i,s);break}}return{...e,[o]:n}}};import Kt from"dotenv";var j=class extends y{async execute(t,e){let o={...e},s=`${this.templatesAccess.getWorkspacePath()}/services.env`,i=Kt.parse(await E(s));Object.assign(o,i),Object.entries(t.context||{}).forEach(([G,wt])=>{o[G]=this.scriptExecutor.executeFormula(wt,o)});let{kcVersion:a,authServerUrl:c,authUser:n,authPassword:u,authTenant:m}=o,h=this.templatesAccess.getWorkspacePath(),v=`docker run --pull=always --env-file ${h}/.env --mount type=bind,src="${h}/config/secret-templates",target="/secret-templates,readonly" --mount type=bind,src="${h}/config/services-config",target="/output" --rm -i ghcr.io/cycrilabs/keycloak-configurator:${a} export-secrets -s ${c} -u ${n} -p "${u}" -r ${m} -c //secret-templates -o //output`;return await O(v,this.templatesAccess.getWorkspacePath()),o}};var q={"custom-prompt":L,"generate-service-configuration":j,"change-env-var-value":B};var f=class r{constructor(t,e,o,s){this.templatesAccess=t;this.useCasesRepository=e;this.contextCreator=o;this.scriptExecutor=s}static create(t,e,o){return new r(e,R.create(e),M.create(t,e),o||$.create())}async run(t,e={}){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,e)}else return await this.#t(t,e)}async#t(t,e={}){let{name:o,description:s,steps:i}=t,a=await this.contextCreator.createContext(e);return p.log(`Running use case ${C(o)}...`),s&&p.log(`${s}`),i&&i.length!==0?(p.log("Executing steps..."),await this.#s(i,a)):a}async#s(t,e){let o={...e,STEPS:t};for(let s=0;s{let o=this.scriptExecutor.executeFormula(t.formula,e);return t.outputFile&&typeof o=="string"&&await F(t.outputFile,o),{...e,[t.resultVariable||"STEP_RESULT"]:o}})}async#a(t,e){if(!t.command)throw new Error("Command missing in step.");return await this.#o(t,e,async()=>{let o=this.scriptExecutor.executeFormula(t.command,e);return await O(o,this.templatesAccess.getWorkspacePath()),e})}async#c(t,e){if(!t.executor||!q[t.executor])throw new Error("Executor missing or not found: "+t.executor);return this.#r(t.executor).execute(t,e)}async#p(t,e){return this.#r("custom-prompt").execute(t,e)}#r(t){return new q[t](this.scriptExecutor,this.templatesAccess)}async#m(t,e){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 p.log("Starting to run references use case..."),await this.run(s,e)}async#o(t,e,o){try{return await o()}catch(s){if(t.catchErrors)return p.error(`Error caught: ${s.message}. Continuing.`),e;throw s}}};async function zt(r){return d(r)&&typeof r=="string"?r.trim():await I("toggle","Do you want to use a template repository?")?await I("text","Enter the name of the template repository"):null}async function xt(r,t){let e=await J("Path to the target workspace directory",r),o=S(e);if(await b(o))throw new Error("A workspace for the given path is already existing.");let i=await J("What is the name of your organization?"),a=await zt(t.templatesRepository),c=st(o,i,a),n=g.create(c);await n.initWorkspace(),await f.create(c,n).run("init")}var Jt=new Ht("--templates-repository ","use a template repository"),yt=new Vt().name("init").description("initialize the workspace").addArgument(Q).addOption(Jt).action(xt);import{Command as qt,Option as Xt}from"commander";var Qt=new Xt("-u, --use-case [use-case]","execute the use case with the given name");async function Yt(r,t){let e=await W(r),o=g.create(e),s=f.create(e,o),a=await R.create(o).loadUseCases("INITIAL"),c=await gt("Use case",t.useCase,a,{createTitle:n=>`${n.name} (${n.id})`,displayFilter:n=>n.state!=="HIDDEN"});await s.run(c)}var dt=new qt().name("run").description("run a use case in the workspace").addArgument(D).addOption(Qt).action(Yt);import{Command as Zt}from"commander";async function te(r){let t=await W(r),e=g.create(t);await e.syncTemplates(),await f.create(t,e).run("sync")}var ht=new Zt().name("sync").description("syncs the workspace templates with the package & configured repository").addArgument(D).action(te);var Ct=()=>process.exit(0);process.on("SIGINT",Ct);process.on("SIGTERM",Ct);process.on("uncaughtException",r=>{p.error(r.message),process.exit(1)});new ee().name(U.name).description(U.description).version(U.version).addCommand(yt).addCommand(dt).addCommand(ht).parseAsync().catch(r=>{p.error(r.message),process.exit(1)}).finally(()=>p.success("Finished execution.")); +import{Command as oe}from"commander";var U={name:"@cycrilabs/ws-ctrl",version:"1.3.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 Jt,Option as qt}from"commander";import{Argument as Tt}from"commander";var X=()=>new Tt("[workspace-path]","path to the workspace"),Q=X(),D=X().default(".");import Ot from"conf";import St from"chalk";var{red:Y,yellow:Z,green:tt,bold:w}=St;import{promises as C}from"node:fs";import{extname as Rt,join as K}from"node:path";async function E(r){return C.readFile(r,"utf8")}async function b(r,t){return C.writeFile(r,t,"utf8")}async function A(r,t,e=[]){try{await C.mkdir(t,{recursive:!0})}catch(s){if(s instanceof Error&&"code"in s&&s.code!=="EEXIST")throw s}let o=await C.readdir(r);await Promise.all(o.filter(s=>!e.includes(s)).map(async s=>{let i=K(r,s),a=K(t,s);(await C.stat(i)).isDirectory()?await A(i,a):await C.copyFile(i,a)}))}async function S(r){try{let o=(await C.readdir(r)).filter(i=>Rt(i).toLowerCase()===".json").map(async i=>{let a=K(r,i),p=await E(a);try{return JSON.parse(p)}catch(n){throw new Error(`Failed to parse JSON in file ${i}: ${n.message}`)}});return await Promise.all(o)}catch(t){throw new Error(`Error loading JSON files: ${t.message}`)}}import{spawnSync as et}from"node:child_process";import{existsSync as vt}from"node:fs";import{join as kt}from"node:path";async function rt(r,t,e){return vt(kt(e,".git"))?et("git",["pull"],{cwd:e,stdio:"inherit"}):et("git",["clone",r,e],{cwd:t,stdio:"inherit"}),Promise.resolve()}var z=class{log(t,e){let o=e?e(t):t;console.log(o)}error(t){this.log(t,Y)}warn(t){this.log(t,Z)}success(t){this.log(t,tt)}},c=new z;import{execSync as At}from"node:child_process";async function O(r,t){return At(r,{cwd:t,stdio:"inherit"})}import{createHash as It}from"node:crypto";import{access as ot,constants as Ut}from"node:fs/promises";import{join as Dt,resolve as bt}from"node:path";function P(r){return bt(r)}function V(r){let t=P(r);return It("md5").update(t).digest("hex").substring(0,10)}async function F(r){try{await ot(r)}catch{return c.error('The provided workspacePath "'+r+'" does not exist.'),!1}let t=V(r);try{return await ot(Dt(r,`${t}.json`),Ut.F_OK),!0}catch{return c.error('Expected to find the file "'+t+'.json" but was not found.'),!1}}var Ft={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},T;function st(r){return T=new Ot({schema:Ft,cwd:P(r),configName:V(r)}),T}function Wt(r){return{workspacePath:P(r),organization:"none",templatesRepository:null}}function it(r,t,e){return T=st(r),T.set("workspacePath",P(r)),T.set("organization",t),T.set("templatesRepository",e),T}async function W(r,t=!1){let e=r.trim();if(t)return c.log("Running within non-workspace directory..."),Wt(e);if(!await F(e))throw new Error("The given path is no valid workspace.");return st(e).store}import{join as l}from"node:path";var nt="templates",x="config",at="docker",ct="git-templates";var pt="repositories",mt="servers",ut="use-cases",H="development";import{dirname as $t}from"node:path";import{fileURLToPath as Nt}from"node:url";function lt(){let r=Nt(import.meta.url);return $t(r)}var g=class r{constructor(t){this.config=t}static create(t){return new r(t)}getPackageTemplatesDir(){return l(this.#t(1),nt)}getGitTemplatesDir(){return l(this.#t(),x,ct)}getConfigDir(){return l(this.#t(),x)}getDockerDir(){return l(this.#t(),x,at)}getServersDir(){return l(this.#t(),x,mt)}getUseCasesDir(){return l(this.#t(),x,ut)}getRepositoriesDir(){return l(this.#t(),x,pt)}getDevelopmentDir(){return l(this.#t(),x,H)}getWorkingDir(){return l(this.#t(),H)}#t(t=0){return t===0?this.getWorkspacePath():lt()}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();c.log(`Syncing templates from ${t}...`),await A(t,this.getWorkspacePath()),await this.syncRepositoryTemplates()}async syncRepositoryTemplates(){let t=this.getTemplatesRepository();if(t){let e=this.createRepositoryUrl(t);c.log(`Syncing templates from repository ${e}...`),await rt(e,this.getWorkspacePath(),this.getGitTemplatesDir()),await A(this.getGitTemplatesDir(),this.getConfigDir(),[".git"])}}async copyDevelopmentDirectory(){await A(this.getDevelopmentDir(),this.getWorkingDir())}};var R=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadUseCases(...t){let e=["DISABLED",...t];return this.loadFiles().then(o=>o.map(s=>this.#t(s))).then(o=>o.filter(s=>!e.includes(s.state)))}async loadFiles(){return S(this.templatesAccess.getUseCasesDir())}#t(t){return{...t,description:t.description||"",state:t.state||"ENABLED"}}};var $=class r{static create(){return new r}executeFormula(t,e){try{let o=this.#t(t,e);return(0,eval)(o)}catch(o){throw new Error(`Error executing formula: ${o}`)}}#t(t,e){return`${Object.entries(e).reduce((s,[i,a])=>`${s}const ${i} = ${JSON.stringify(a)};`,"")}${t}`}};var N=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadRepositories(){return this.loadFiles().then(t=>t.map(e=>this.#t(e)))}async loadFiles(){return S(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 _=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadServers(){return this.loadFiles()}async loadFiles(){return S(this.templatesAccess.getServersDir())}};var B=class r{constructor(t,e,o){this.templatesAccess=t;this.serversRepository=e;this.repositoriesRepository=o}static create(t){return new r(t,_.create(t),N.create(t))}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()}}};var d=class{constructor(t,e){this.scriptExecutor=t;this.templatesAccess=e}async execute(t,e){throw new Error("Method not implemented.")}};var M=class extends d{async execute(t,e){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,e),p=await E(a),{name:n,value:u}=i,m=this.scriptExecutor.executeFormula(n,e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),h=this.scriptExecutor.executeFormula(u,e),k=p.replace(new RegExp(`${m}=.*`),`${m}=${h}`),G=this.scriptExecutor.executeFormula(s||o,e);return await b(G,k),e}};import Gt from"prompts";import _t from"prompts";function v(r){r.aborted&&(process.stdout.write("\x1B[?25h"),process.stdout.write(` +`),process.exit(1))}function y(r){return typeof r=="string"&&!!r.trim()||typeof r=="boolean"}async function Bt(r,t,e,o={}){return _t({onState:v,type:r,name:"entity",message:`Select the ${t}`,choices:e.filter(o.displayFilter||(()=>!0)).map(s=>({title:o.createTitle?o.createTitle(s):s.name,value:s.id}))}).then(({entity:s})=>s)}function Mt(r,t){let e=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof r=="string"){let o=e[r.trim()];if(o)return[o]}else{if(Array.isArray(r))return r.map(o=>e[o.trim()]);throw new Error(`Can find entity for value ${r}.`)}throw new Error(`Entity for selection ${w(r)} not found.`)}async function Lt(r,t,e,o,s={}){let i=y(e)&&typeof e=="string"?e:await Bt(r,t,o,s);return Mt(i,o)}async function gt(r,t,e,o={}){let[s]=await Lt("select",r,t,e,o);return s}import jt from"prompts";async function ft(r,t){return jt({onState:v,type:r,name:"input",message:t,initial:r==="toggle"?!0:void 0,active:"yes",inactive:"no"}).then(({input:e})=>e)}async function I(r,t,e){return y(e)?e:ft(r,t)}async function J(r,t){let e=y(t)?t:await ft("text",r);if(y(e)&&typeof e=="string")return e.trim();throw Error("No input path provided.")}async function Kt(r,t,e){return Gt({onState:v,type:r,name:"input",message:t,choices:e.map(({id:o,name:s})=>({title:s,value:o}))}).then(({input:o})=>o)}function zt(r,t){let e=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof r=="string"){let o=e[r.trim()];if(o)return o}else{if(Array.isArray(r))return r.map(o=>e[o.trim()]);throw new Error(`Can find entity for value ${r}.`)}throw new Error(`Entity for selection ${w(r)} not found.`)}var L=class extends d{async execute(t,e){let{name:o,message:s,type:i,entities:a,entityKey:p}=t.context;if(!o||!s||!i)throw new Error("Prompt name, message or type missing.");let n;switch(i){case"select":case"multiselect":{let u=(p?e[p]:a)||[],m=await Kt(i,s,u);n=zt(m,u);break}default:{n=await I(i,s);break}}return{...e,[o]:n}}};import Vt from"dotenv";var j=class extends d{async execute(t,e){let o={...e},s=`${this.templatesAccess.getWorkspacePath()}/services.env`,i=Vt.parse(await E(s));Object.assign(o,i),Object.entries(t.context||{}).forEach(([G,Et])=>{o[G]=this.scriptExecutor.executeFormula(Et,o)});let{kcVersion:a,authServerUrl:p,authUser:n,authPassword:u,authTenant:m}=o,h=this.templatesAccess.getWorkspacePath(),k=`docker run --pull=always --env-file ${h}/.env --mount type=bind,src="${h}/config/secret-templates",target="/secret-templates,readonly" --mount type=bind,src="${h}/config/services-config",target="/output" --rm -i ghcr.io/cycrilabs/keycloak-configurator:${a} export-secrets -s ${p} -u ${n} -p "${u}" -r ${m} -c //secret-templates -o //output`;return await O(k,this.templatesAccess.getWorkspacePath()),o}};var q={"custom-prompt":L,"generate-service-configuration":j,"change-env-var-value":M};var f=class r{constructor(t,e,o,s){this.templatesAccess=t;this.useCasesRepository=e;this.contextCreator=o;this.scriptExecutor=s}static create(t,e){return new r(t,R.create(t),B.create(t),e||$.create())}async run(t,e={}){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,e)}else return await this.#t(t,e)}async#t(t,e={}){let{name:o,description:s,steps:i}=t,a=await this.contextCreator.createContext(e);return c.log(`Running use case ${w(o)}...`),s&&c.log(`${s}`),i&&i.length!==0?(c.log("Executing steps..."),await this.#s(i,a)):a}async#s(t,e){let o={...e,STEPS:t};for(let s=0;s{let o=this.scriptExecutor.executeFormula(t.formula,e);return t.outputFile&&typeof o=="string"&&await b(t.outputFile,o),{...e,[t.resultVariable||"STEP_RESULT"]:o}})}async#a(t,e){if(!t.command)throw new Error("Command missing in step.");return await this.#o(t,e,async()=>{let o=this.scriptExecutor.executeFormula(t.command,e);return await O(o,this.templatesAccess.getWorkspacePath()),e})}async#c(t,e){if(!t.executor||!q[t.executor])throw new Error("Executor missing or not found: "+t.executor);return this.#r(t.executor).execute(t,e)}async#p(t,e){return this.#r("custom-prompt").execute(t,e)}#r(t){return new q[t](this.scriptExecutor,this.templatesAccess)}async#m(t,e){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 c.log("Starting to run references use case..."),await this.run(s,e)}async#o(t,e,o){try{return await o()}catch(s){if(t.catchErrors)return c.error(`Error caught: ${s.message}. Continuing.`),e;throw s}}};async function Ht(r){return y(r)&&typeof r=="string"?r.trim():await I("toggle","Do you want to use a template repository?")?await I("text","Enter the name of the template repository"):null}async function xt(r,t){let e=await J("Path to the target workspace directory",r),o=P(e);if(await F(o))throw new Error("A workspace for the given path is already existing.");let i=await J("What is the name of your organization?"),a=await Ht(t.templatesRepository),p=it(o,i,a),n=g.create(p.store);await n.initWorkspace(),await f.create(n).run("init")}var Xt=new qt("--templates-repository ","use a template repository"),dt=new Jt().name("init").description("initialize the workspace").addArgument(Q).addOption(Xt).action(xt);import{Command as Qt,Option as yt}from"commander";var Yt=new yt("-u, --use-case [use-case]","execute the use case with the given name"),Zt=new yt("--debug","enable debug mode");async function te(r,t){let e=await W(r,t.debug),o=g.create(e),s=f.create(o),a=await R.create(o).loadUseCases("INITIAL"),p=await gt("Use case",t.useCase,a,{createTitle:n=>`${n.name} (${n.id})`,displayFilter:n=>n.state!=="HIDDEN"});await s.run(p)}var ht=new Qt().name("run").description("run a use case in the workspace").addArgument(D).addOption(Yt).addOption(Zt).action(te);import{Command as ee}from"commander";async function re(r){let t=await W(r),e=g.create(t);await e.syncTemplates(),await f.create(e).run("sync")}var wt=new ee().name("sync").description("syncs the workspace templates with the package & configured repository").addArgument(D).action(re);var Ct=()=>process.exit(0);process.on("SIGINT",Ct);process.on("SIGTERM",Ct);process.on("uncaughtException",r=>{c.error(r.message),process.exit(1)});new oe().name(U.name).description(U.description).version(U.version).addCommand(dt).addCommand(ht).addCommand(wt).parseAsync().catch(r=>{c.error(r.message),process.exit(1)}).finally(()=>c.success("Finished execution.")); diff --git a/src/commands/init/action.ts b/src/commands/init/action.ts index 1f1e0ec..086f3ed 100644 --- a/src/commands/init/action.ts +++ b/src/commands/init/action.ts @@ -65,9 +65,9 @@ export async function init( const config = initConfig(workspacePath, organization, templatesRepository); // copy templates from package/repo to workspace - const templatesAccess = TemplatesAccess.create(config); + const templatesAccess = TemplatesAccess.create(config.store); await templatesAccess.initWorkspace(); - const useCaseRunner = UseCaseRunner.create(config, templatesAccess); + const useCaseRunner = UseCaseRunner.create(templatesAccess); await useCaseRunner.run('init'); } diff --git a/src/commands/run/run.ts b/src/commands/run/run.ts index 1c53948..1ac08e5 100644 --- a/src/commands/run/run.ts +++ b/src/commands/run/run.ts @@ -10,18 +10,20 @@ const useCaseOption = new Option( '-u, --use-case [use-case]', 'execute the use case with the given name' ); +const debugOption = new Option('--debug', 'enable debug mode'); interface RunActionOptions { useCase: OptionInput; + debug: boolean; } export async function runAction( workspacePathRaw: string, options: RunActionOptions ) { - const config = await loadWorkspaceConfig(workspacePathRaw); + const config = await loadWorkspaceConfig(workspacePathRaw, options.debug); const templatesAccess = TemplatesAccess.create(config); - const useCaseRunner = UseCaseRunner.create(config, templatesAccess); + const useCaseRunner = UseCaseRunner.create(templatesAccess); const useCaseRepository = UseCasesRepository.create(templatesAccess); const useCases = await useCaseRepository.loadUseCases('INITIAL'); @@ -43,4 +45,5 @@ export const run = new Command() .description('run a use case in the workspace') .addArgument(defaultWorkspacePathArgument) .addOption(useCaseOption) + .addOption(debugOption) .action(runAction); diff --git a/src/commands/sync/sync.ts b/src/commands/sync/sync.ts index 42fceca..5dfe012 100644 --- a/src/commands/sync/sync.ts +++ b/src/commands/sync/sync.ts @@ -8,7 +8,7 @@ async function syncAction(workspacePathRaw: string): Promise { const templatesAccess = TemplatesAccess.create(config); await templatesAccess.syncTemplates(); - const useCaseRunner = UseCaseRunner.create(config, templatesAccess); + const useCaseRunner = UseCaseRunner.create(templatesAccess); await useCaseRunner.run('sync'); } diff --git a/src/config.ts b/src/config.ts index 0b9140e..2a212a5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,10 +2,11 @@ import Conf from 'conf'; import { getWorkspacePathIdentifier, isExistingWorkspace, + logger, resolveWorkspacePath, } from './utils/index.js'; -interface Config { +export interface Config { workspacePath: string; organization: string; templatesRepository: string | null; @@ -27,6 +28,23 @@ const schema = { export let config: WorkspaceConfig; +function loadConfig(workspacePath: string): WorkspaceConfig { + config = new Conf({ + schema, + cwd: resolveWorkspacePath(workspacePath), + configName: getWorkspacePathIdentifier(workspacePath), + }); + return config; +} + +function loadBlankConfig(workspacePath: string): Config { + return { + workspacePath: resolveWorkspacePath(workspacePath), + organization: 'none', + templatesRepository: null, + }; +} + export function initConfig( workspacePath: string, organization: string, @@ -39,20 +57,19 @@ export function initConfig( return config; } -export function loadConfig(workspacePath: string): WorkspaceConfig { - config = new Conf({ - schema, - cwd: resolveWorkspacePath(workspacePath), - configName: getWorkspacePathIdentifier(workspacePath), - }); - return config; -} - -export async function loadWorkspaceConfig(workspacePathRaw: string) { +export async function loadWorkspaceConfig( + workspacePathRaw: string, + debug: boolean = false +): Promise { const workspacePath = workspacePathRaw.trim(); + if (debug) { + logger.log('Running within non-workspace directory...'); + return loadBlankConfig(workspacePath); + } + const existingWorkspace = await isExistingWorkspace(workspacePath); if (!existingWorkspace) { throw new Error(`The given path is no valid workspace.`); } - return loadConfig(workspacePath); + return loadConfig(workspacePath).store; } diff --git a/src/services/access/templates-access.ts b/src/services/access/templates-access.ts index 8eb4c70..271e350 100644 --- a/src/services/access/templates-access.ts +++ b/src/services/access/templates-access.ts @@ -1,5 +1,5 @@ import { join } from 'node:path'; -import { WorkspaceConfig } from '../../config.js'; +import { Config } from '../../config.js'; import { copyDirectory, gitUpdate, logger } from '../../utils/index.js'; import { DIR_CONFIG, @@ -19,11 +19,11 @@ enum Location { } export class TemplatesAccess { - static create(config: WorkspaceConfig): TemplatesAccess { + static create(config: Config): TemplatesAccess { return new TemplatesAccess(config); } - constructor(private readonly config: WorkspaceConfig) {} + constructor(private readonly config: Config) {} getPackageTemplatesDir(): string { return join(this.#getBaseDir(Location.PACKAGE), DIR_TEMPLATES); @@ -68,19 +68,19 @@ export class TemplatesAccess { } getWorkspacePath(): string { - return this.config.get('workspacePath'); + return this.config.workspacePath; } getTemplatesRepository(): string | null { - return this.config.get('templatesRepository'); + return this.config.templatesRepository; } - createRepositoryUrl(organization: string, name: string): string { + createRepositoryUrl(name: string): string { // TODO: // - add support for HTTPS URLs when provided in config // - add support for different repository hosts // - add support for different tenants - return `git@bitbucket.org:${organization}/${name}.git`; + return `git@bitbucket.org:${this.config.organization}/${name}.git`; } async initWorkspace(): Promise { @@ -102,10 +102,7 @@ export class TemplatesAccess { // sync the templates from the repository if configured const templatesRepository = this.getTemplatesRepository(); if (templatesRepository) { - const url = this.createRepositoryUrl( - this.config.get('organization'), - templatesRepository - ); + const url = this.createRepositoryUrl(templatesRepository); logger.log(`Syncing templates from repository ${url}...`); await gitUpdate(url, this.getWorkspacePath(), this.getGitTemplatesDir()); diff --git a/src/services/repositories.repository.ts b/src/services/repositories.repository.ts index e08fd1e..b3d162d 100644 --- a/src/services/repositories.repository.ts +++ b/src/services/repositories.repository.ts @@ -1,17 +1,13 @@ -import { WorkspaceConfig } from '../config.js'; import { Repository } from '../types/index.js'; import { loadFilesFromDirectory } from '../utils/index.js'; import { TemplatesAccess } from './access/index.js'; export class RepositoriesRepository { - static create(config: WorkspaceConfig, templatesAccess: TemplatesAccess) { - return new RepositoriesRepository(config, templatesAccess); + static create(templatesAccess: TemplatesAccess) { + return new RepositoriesRepository(templatesAccess); } - constructor( - private readonly config: WorkspaceConfig, - private readonly templatesAccess: TemplatesAccess - ) {} + constructor(private readonly templatesAccess: TemplatesAccess) {} async loadRepositories(): Promise[]> { return this.loadFiles().then(repositories => @@ -31,10 +27,7 @@ export class RepositoriesRepository { alias: repository.alias || repository.name, url: repository.url || - this.templatesAccess.createRepositoryUrl( - this.config.get('organization'), - repository.name - ), + this.templatesAccess.createRepositoryUrl(repository.name), attributes: { type: 'UNKNOWN', ...repository.attributes, diff --git a/src/services/use-cases/context-creator.ts b/src/services/use-cases/context-creator.ts index 67ce604..3c09384 100644 --- a/src/services/use-cases/context-creator.ts +++ b/src/services/use-cases/context-creator.ts @@ -1,15 +1,14 @@ -import { WorkspaceConfig } from '../../config.js'; import { Context } from '../../types/index.js'; import { TemplatesAccess } from '../access/index.js'; import { RepositoriesRepository } from '../repositories.repository.js'; import { ServersRepository } from '../servers.repository.js'; export class ContextCreator { - static create(config: WorkspaceConfig, templatesAccess: TemplatesAccess) { + static create(templatesAccess: TemplatesAccess) { return new ContextCreator( templatesAccess, ServersRepository.create(templatesAccess), - RepositoriesRepository.create(config, templatesAccess) + RepositoriesRepository.create(templatesAccess) ); } diff --git a/src/services/use-cases/use-case-runner.ts b/src/services/use-cases/use-case-runner.ts index 001d8b6..96ca903 100644 --- a/src/services/use-cases/use-case-runner.ts +++ b/src/services/use-cases/use-case-runner.ts @@ -1,4 +1,3 @@ -import { WorkspaceConfig } from '../../config.js'; import { Context, UseCase, UseCaseStep } from '../../types/index.js'; import { bold, @@ -16,14 +15,13 @@ import { executorMapping } from './executors/index.js'; export class UseCaseRunner { static create( - config: WorkspaceConfig, templatesAccess: TemplatesAccess, scriptExecutor?: ScriptExecutor ): UseCaseRunner { return new UseCaseRunner( templatesAccess, UseCasesRepository.create(templatesAccess), - ContextCreator.create(config, templatesAccess), + ContextCreator.create(templatesAccess), scriptExecutor || ScriptExecutor.create() ); } From 56edf51e7cfd5ba96b7d7c471eeeb4ad63d65636 Mon Sep 17 00:00:00 2001 From: Marc Scheib Date: Sat, 26 Apr 2025 13:30:42 +0200 Subject: [PATCH 2/2] feat(run): add OS variable to use case context (#3) --- README.md | 1 + assets/templates/config/use-cases/check-requirements.json | 5 +++++ dist/index.js | 2 +- dist/templates/config/use-cases/check-requirements.json | 5 +++++ src/services/repositories.repository.test.ts | 4 ++-- src/services/servers.repository.test.ts | 2 +- src/services/use-cases.repository.test.ts | 2 +- src/services/use-cases/context-creator.ts | 2 ++ src/services/use-cases/use-case-runner.test.ts | 8 ++------ src/utils/processes.ts | 2 ++ 10 files changed, 22 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a91148d..d8e4ad8 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ All steps are working with a context object that is empty initially. Whenever a use case starts, the context is filled with the following properties. Throughout execution, steps can add or modify properties in the context object. +- `OS`: the operating system where the CLI is executed on; represents the result of [`process.platform`](https://nodejs.org/api/process.html#process_process_platform) - `WORKSPACE_PATH`: the absolute path to the workspace - `WORKING_DIR`: the absolute path to the working directory, see [Workspace structure](#workspace-structure) - `SERVERS`: the list of all servers, see [Server configuration](#server-configuration) diff --git a/assets/templates/config/use-cases/check-requirements.json b/assets/templates/config/use-cases/check-requirements.json index 295d663..383e58b 100644 --- a/assets/templates/config/use-cases/check-requirements.json +++ b/assets/templates/config/use-cases/check-requirements.json @@ -4,6 +4,11 @@ "name": "Check requirements", "description": "This use case checks the requirements to develop locally.", "steps": [ + { + "type": "FORMULA", + "description": "Print OS info", + "formula": "console.log(`Running on ${OS}`)" + }, { "type": "COMMAND", "description": "Execute commands", diff --git a/dist/index.js b/dist/index.js index 300e19f..b1f6a68 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,3 +1,3 @@ #!/usr/bin/env node -import{Command as oe}from"commander";var U={name:"@cycrilabs/ws-ctrl",version:"1.3.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 Jt,Option as qt}from"commander";import{Argument as Tt}from"commander";var X=()=>new Tt("[workspace-path]","path to the workspace"),Q=X(),D=X().default(".");import Ot from"conf";import St from"chalk";var{red:Y,yellow:Z,green:tt,bold:w}=St;import{promises as C}from"node:fs";import{extname as Rt,join as K}from"node:path";async function E(r){return C.readFile(r,"utf8")}async function b(r,t){return C.writeFile(r,t,"utf8")}async function A(r,t,e=[]){try{await C.mkdir(t,{recursive:!0})}catch(s){if(s instanceof Error&&"code"in s&&s.code!=="EEXIST")throw s}let o=await C.readdir(r);await Promise.all(o.filter(s=>!e.includes(s)).map(async s=>{let i=K(r,s),a=K(t,s);(await C.stat(i)).isDirectory()?await A(i,a):await C.copyFile(i,a)}))}async function S(r){try{let o=(await C.readdir(r)).filter(i=>Rt(i).toLowerCase()===".json").map(async i=>{let a=K(r,i),p=await E(a);try{return JSON.parse(p)}catch(n){throw new Error(`Failed to parse JSON in file ${i}: ${n.message}`)}});return await Promise.all(o)}catch(t){throw new Error(`Error loading JSON files: ${t.message}`)}}import{spawnSync as et}from"node:child_process";import{existsSync as vt}from"node:fs";import{join as kt}from"node:path";async function rt(r,t,e){return vt(kt(e,".git"))?et("git",["pull"],{cwd:e,stdio:"inherit"}):et("git",["clone",r,e],{cwd:t,stdio:"inherit"}),Promise.resolve()}var z=class{log(t,e){let o=e?e(t):t;console.log(o)}error(t){this.log(t,Y)}warn(t){this.log(t,Z)}success(t){this.log(t,tt)}},c=new z;import{execSync as At}from"node:child_process";async function O(r,t){return At(r,{cwd:t,stdio:"inherit"})}import{createHash as It}from"node:crypto";import{access as ot,constants as Ut}from"node:fs/promises";import{join as Dt,resolve as bt}from"node:path";function P(r){return bt(r)}function V(r){let t=P(r);return It("md5").update(t).digest("hex").substring(0,10)}async function F(r){try{await ot(r)}catch{return c.error('The provided workspacePath "'+r+'" does not exist.'),!1}let t=V(r);try{return await ot(Dt(r,`${t}.json`),Ut.F_OK),!0}catch{return c.error('Expected to find the file "'+t+'.json" but was not found.'),!1}}var Ft={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},T;function st(r){return T=new Ot({schema:Ft,cwd:P(r),configName:V(r)}),T}function Wt(r){return{workspacePath:P(r),organization:"none",templatesRepository:null}}function it(r,t,e){return T=st(r),T.set("workspacePath",P(r)),T.set("organization",t),T.set("templatesRepository",e),T}async function W(r,t=!1){let e=r.trim();if(t)return c.log("Running within non-workspace directory..."),Wt(e);if(!await F(e))throw new Error("The given path is no valid workspace.");return st(e).store}import{join as l}from"node:path";var nt="templates",x="config",at="docker",ct="git-templates";var pt="repositories",mt="servers",ut="use-cases",H="development";import{dirname as $t}from"node:path";import{fileURLToPath as Nt}from"node:url";function lt(){let r=Nt(import.meta.url);return $t(r)}var g=class r{constructor(t){this.config=t}static create(t){return new r(t)}getPackageTemplatesDir(){return l(this.#t(1),nt)}getGitTemplatesDir(){return l(this.#t(),x,ct)}getConfigDir(){return l(this.#t(),x)}getDockerDir(){return l(this.#t(),x,at)}getServersDir(){return l(this.#t(),x,mt)}getUseCasesDir(){return l(this.#t(),x,ut)}getRepositoriesDir(){return l(this.#t(),x,pt)}getDevelopmentDir(){return l(this.#t(),x,H)}getWorkingDir(){return l(this.#t(),H)}#t(t=0){return t===0?this.getWorkspacePath():lt()}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();c.log(`Syncing templates from ${t}...`),await A(t,this.getWorkspacePath()),await this.syncRepositoryTemplates()}async syncRepositoryTemplates(){let t=this.getTemplatesRepository();if(t){let e=this.createRepositoryUrl(t);c.log(`Syncing templates from repository ${e}...`),await rt(e,this.getWorkspacePath(),this.getGitTemplatesDir()),await A(this.getGitTemplatesDir(),this.getConfigDir(),[".git"])}}async copyDevelopmentDirectory(){await A(this.getDevelopmentDir(),this.getWorkingDir())}};var R=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadUseCases(...t){let e=["DISABLED",...t];return this.loadFiles().then(o=>o.map(s=>this.#t(s))).then(o=>o.filter(s=>!e.includes(s.state)))}async loadFiles(){return S(this.templatesAccess.getUseCasesDir())}#t(t){return{...t,description:t.description||"",state:t.state||"ENABLED"}}};var $=class r{static create(){return new r}executeFormula(t,e){try{let o=this.#t(t,e);return(0,eval)(o)}catch(o){throw new Error(`Error executing formula: ${o}`)}}#t(t,e){return`${Object.entries(e).reduce((s,[i,a])=>`${s}const ${i} = ${JSON.stringify(a)};`,"")}${t}`}};var N=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadRepositories(){return this.loadFiles().then(t=>t.map(e=>this.#t(e)))}async loadFiles(){return S(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 _=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadServers(){return this.loadFiles()}async loadFiles(){return S(this.templatesAccess.getServersDir())}};var B=class r{constructor(t,e,o){this.templatesAccess=t;this.serversRepository=e;this.repositoriesRepository=o}static create(t){return new r(t,_.create(t),N.create(t))}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()}}};var d=class{constructor(t,e){this.scriptExecutor=t;this.templatesAccess=e}async execute(t,e){throw new Error("Method not implemented.")}};var M=class extends d{async execute(t,e){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,e),p=await E(a),{name:n,value:u}=i,m=this.scriptExecutor.executeFormula(n,e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),h=this.scriptExecutor.executeFormula(u,e),k=p.replace(new RegExp(`${m}=.*`),`${m}=${h}`),G=this.scriptExecutor.executeFormula(s||o,e);return await b(G,k),e}};import Gt from"prompts";import _t from"prompts";function v(r){r.aborted&&(process.stdout.write("\x1B[?25h"),process.stdout.write(` +import{Command as oe}from"commander";var U={name:"@cycrilabs/ws-ctrl",version:"1.3.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 Jt,Option as qt}from"commander";import{Argument as Tt}from"commander";var X=()=>new Tt("[workspace-path]","path to the workspace"),Q=X(),D=X().default(".");import Ot from"conf";import St from"chalk";var{red:Y,yellow:Z,green:tt,bold:w}=St;import{promises as C}from"node:fs";import{extname as Rt,join as K}from"node:path";async function E(r){return C.readFile(r,"utf8")}async function b(r,t){return C.writeFile(r,t,"utf8")}async function A(r,t,e=[]){try{await C.mkdir(t,{recursive:!0})}catch(s){if(s instanceof Error&&"code"in s&&s.code!=="EEXIST")throw s}let o=await C.readdir(r);await Promise.all(o.filter(s=>!e.includes(s)).map(async s=>{let i=K(r,s),a=K(t,s);(await C.stat(i)).isDirectory()?await A(i,a):await C.copyFile(i,a)}))}async function S(r){try{let o=(await C.readdir(r)).filter(i=>Rt(i).toLowerCase()===".json").map(async i=>{let a=K(r,i),p=await E(a);try{return JSON.parse(p)}catch(n){throw new Error(`Failed to parse JSON in file ${i}: ${n.message}`)}});return await Promise.all(o)}catch(t){throw new Error(`Error loading JSON files: ${t.message}`)}}import{spawnSync as et}from"node:child_process";import{existsSync as vt}from"node:fs";import{join as kt}from"node:path";async function rt(r,t,e){return vt(kt(e,".git"))?et("git",["pull"],{cwd:e,stdio:"inherit"}):et("git",["clone",r,e],{cwd:t,stdio:"inherit"}),Promise.resolve()}var z=class{log(t,e){let o=e?e(t):t;console.log(o)}error(t){this.log(t,Y)}warn(t){this.log(t,Z)}success(t){this.log(t,tt)}},c=new z;import{execSync as At}from"node:child_process";async function O(r,t){return At(r,{cwd:t,stdio:"inherit"})}import{createHash as It}from"node:crypto";import{access as ot,constants as Ut}from"node:fs/promises";import{join as Dt,resolve as bt}from"node:path";function P(r){return bt(r)}function V(r){let t=P(r);return It("md5").update(t).digest("hex").substring(0,10)}async function F(r){try{await ot(r)}catch{return c.error('The provided workspacePath "'+r+'" does not exist.'),!1}let t=V(r);try{return await ot(Dt(r,`${t}.json`),Ut.F_OK),!0}catch{return c.error('Expected to find the file "'+t+'.json" but was not found.'),!1}}var Ft={workspacePath:{type:"string"},organization:{type:"string"},templatesRepository:{type:["string","null"]}},T;function st(r){return T=new Ot({schema:Ft,cwd:P(r),configName:V(r)}),T}function Wt(r){return{workspacePath:P(r),organization:"none",templatesRepository:null}}function it(r,t,e){return T=st(r),T.set("workspacePath",P(r)),T.set("organization",t),T.set("templatesRepository",e),T}async function W(r,t=!1){let e=r.trim();if(t)return c.log("Running within non-workspace directory..."),Wt(e);if(!await F(e))throw new Error("The given path is no valid workspace.");return st(e).store}import{join as l}from"node:path";var nt="templates",x="config",at="docker",ct="git-templates";var pt="repositories",mt="servers",ut="use-cases",H="development";import{dirname as $t}from"node:path";import{fileURLToPath as Nt}from"node:url";function lt(){let r=Nt(import.meta.url);return $t(r)}var g=class r{constructor(t){this.config=t}static create(t){return new r(t)}getPackageTemplatesDir(){return l(this.#t(1),nt)}getGitTemplatesDir(){return l(this.#t(),x,ct)}getConfigDir(){return l(this.#t(),x)}getDockerDir(){return l(this.#t(),x,at)}getServersDir(){return l(this.#t(),x,mt)}getUseCasesDir(){return l(this.#t(),x,ut)}getRepositoriesDir(){return l(this.#t(),x,pt)}getDevelopmentDir(){return l(this.#t(),x,H)}getWorkingDir(){return l(this.#t(),H)}#t(t=0){return t===0?this.getWorkspacePath():lt()}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();c.log(`Syncing templates from ${t}...`),await A(t,this.getWorkspacePath()),await this.syncRepositoryTemplates()}async syncRepositoryTemplates(){let t=this.getTemplatesRepository();if(t){let e=this.createRepositoryUrl(t);c.log(`Syncing templates from repository ${e}...`),await rt(e,this.getWorkspacePath(),this.getGitTemplatesDir()),await A(this.getGitTemplatesDir(),this.getConfigDir(),[".git"])}}async copyDevelopmentDirectory(){await A(this.getDevelopmentDir(),this.getWorkingDir())}};var R=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadUseCases(...t){let e=["DISABLED",...t];return this.loadFiles().then(o=>o.map(s=>this.#t(s))).then(o=>o.filter(s=>!e.includes(s.state)))}async loadFiles(){return S(this.templatesAccess.getUseCasesDir())}#t(t){return{...t,description:t.description||"",state:t.state||"ENABLED"}}};var $=class r{static create(){return new r}executeFormula(t,e){try{let o=this.#t(t,e);return(0,eval)(o)}catch(o){throw new Error(`Error executing formula: ${o}`)}}#t(t,e){return`${Object.entries(e).reduce((s,[i,a])=>`${s}const ${i} = ${JSON.stringify(a)};`,"")}${t}`}};var N=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadRepositories(){return this.loadFiles().then(t=>t.map(e=>this.#t(e)))}async loadFiles(){return S(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 _=class r{constructor(t){this.templatesAccess=t}static create(t){return new r(t)}async loadServers(){return this.loadFiles()}async loadFiles(){return S(this.templatesAccess.getServersDir())}};var B=class r{constructor(t,e,o){this.templatesAccess=t;this.serversRepository=e;this.repositoriesRepository=o}static create(t){return new r(t,_.create(t),N.create(t))}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:process.platform}}};var d=class{constructor(t,e){this.scriptExecutor=t;this.templatesAccess=e}async execute(t,e){throw new Error("Method not implemented.")}};var M=class extends d{async execute(t,e){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,e),p=await E(a),{name:n,value:u}=i,m=this.scriptExecutor.executeFormula(n,e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),h=this.scriptExecutor.executeFormula(u,e),k=p.replace(new RegExp(`${m}=.*`),`${m}=${h}`),G=this.scriptExecutor.executeFormula(s||o,e);return await b(G,k),e}};import Gt from"prompts";import _t from"prompts";function v(r){r.aborted&&(process.stdout.write("\x1B[?25h"),process.stdout.write(` `),process.exit(1))}function y(r){return typeof r=="string"&&!!r.trim()||typeof r=="boolean"}async function Bt(r,t,e,o={}){return _t({onState:v,type:r,name:"entity",message:`Select the ${t}`,choices:e.filter(o.displayFilter||(()=>!0)).map(s=>({title:o.createTitle?o.createTitle(s):s.name,value:s.id}))}).then(({entity:s})=>s)}function Mt(r,t){let e=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof r=="string"){let o=e[r.trim()];if(o)return[o]}else{if(Array.isArray(r))return r.map(o=>e[o.trim()]);throw new Error(`Can find entity for value ${r}.`)}throw new Error(`Entity for selection ${w(r)} not found.`)}async function Lt(r,t,e,o,s={}){let i=y(e)&&typeof e=="string"?e:await Bt(r,t,o,s);return Mt(i,o)}async function gt(r,t,e,o={}){let[s]=await Lt("select",r,t,e,o);return s}import jt from"prompts";async function ft(r,t){return jt({onState:v,type:r,name:"input",message:t,initial:r==="toggle"?!0:void 0,active:"yes",inactive:"no"}).then(({input:e})=>e)}async function I(r,t,e){return y(e)?e:ft(r,t)}async function J(r,t){let e=y(t)?t:await ft("text",r);if(y(e)&&typeof e=="string")return e.trim();throw Error("No input path provided.")}async function Kt(r,t,e){return Gt({onState:v,type:r,name:"input",message:t,choices:e.map(({id:o,name:s})=>({title:s,value:o}))}).then(({input:o})=>o)}function zt(r,t){let e=t.reduce((o,s)=>({...o,[s.id]:s}),{});if(typeof r=="string"){let o=e[r.trim()];if(o)return o}else{if(Array.isArray(r))return r.map(o=>e[o.trim()]);throw new Error(`Can find entity for value ${r}.`)}throw new Error(`Entity for selection ${w(r)} not found.`)}var L=class extends d{async execute(t,e){let{name:o,message:s,type:i,entities:a,entityKey:p}=t.context;if(!o||!s||!i)throw new Error("Prompt name, message or type missing.");let n;switch(i){case"select":case"multiselect":{let u=(p?e[p]:a)||[],m=await Kt(i,s,u);n=zt(m,u);break}default:{n=await I(i,s);break}}return{...e,[o]:n}}};import Vt from"dotenv";var j=class extends d{async execute(t,e){let o={...e},s=`${this.templatesAccess.getWorkspacePath()}/services.env`,i=Vt.parse(await E(s));Object.assign(o,i),Object.entries(t.context||{}).forEach(([G,Et])=>{o[G]=this.scriptExecutor.executeFormula(Et,o)});let{kcVersion:a,authServerUrl:p,authUser:n,authPassword:u,authTenant:m}=o,h=this.templatesAccess.getWorkspacePath(),k=`docker run --pull=always --env-file ${h}/.env --mount type=bind,src="${h}/config/secret-templates",target="/secret-templates,readonly" --mount type=bind,src="${h}/config/services-config",target="/output" --rm -i ghcr.io/cycrilabs/keycloak-configurator:${a} export-secrets -s ${p} -u ${n} -p "${u}" -r ${m} -c //secret-templates -o //output`;return await O(k,this.templatesAccess.getWorkspacePath()),o}};var q={"custom-prompt":L,"generate-service-configuration":j,"change-env-var-value":M};var f=class r{constructor(t,e,o,s){this.templatesAccess=t;this.useCasesRepository=e;this.contextCreator=o;this.scriptExecutor=s}static create(t,e){return new r(t,R.create(t),B.create(t),e||$.create())}async run(t,e={}){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,e)}else return await this.#t(t,e)}async#t(t,e={}){let{name:o,description:s,steps:i}=t,a=await this.contextCreator.createContext(e);return c.log(`Running use case ${w(o)}...`),s&&c.log(`${s}`),i&&i.length!==0?(c.log("Executing steps..."),await this.#s(i,a)):a}async#s(t,e){let o={...e,STEPS:t};for(let s=0;s{let o=this.scriptExecutor.executeFormula(t.formula,e);return t.outputFile&&typeof o=="string"&&await b(t.outputFile,o),{...e,[t.resultVariable||"STEP_RESULT"]:o}})}async#a(t,e){if(!t.command)throw new Error("Command missing in step.");return await this.#o(t,e,async()=>{let o=this.scriptExecutor.executeFormula(t.command,e);return await O(o,this.templatesAccess.getWorkspacePath()),e})}async#c(t,e){if(!t.executor||!q[t.executor])throw new Error("Executor missing or not found: "+t.executor);return this.#r(t.executor).execute(t,e)}async#p(t,e){return this.#r("custom-prompt").execute(t,e)}#r(t){return new q[t](this.scriptExecutor,this.templatesAccess)}async#m(t,e){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 c.log("Starting to run references use case..."),await this.run(s,e)}async#o(t,e,o){try{return await o()}catch(s){if(t.catchErrors)return c.error(`Error caught: ${s.message}. Continuing.`),e;throw s}}};async function Ht(r){return y(r)&&typeof r=="string"?r.trim():await I("toggle","Do you want to use a template repository?")?await I("text","Enter the name of the template repository"):null}async function xt(r,t){let e=await J("Path to the target workspace directory",r),o=P(e);if(await F(o))throw new Error("A workspace for the given path is already existing.");let i=await J("What is the name of your organization?"),a=await Ht(t.templatesRepository),p=it(o,i,a),n=g.create(p.store);await n.initWorkspace(),await f.create(n).run("init")}var Xt=new qt("--templates-repository ","use a template repository"),dt=new Jt().name("init").description("initialize the workspace").addArgument(Q).addOption(Xt).action(xt);import{Command as Qt,Option as yt}from"commander";var Yt=new yt("-u, --use-case [use-case]","execute the use case with the given name"),Zt=new yt("--debug","enable debug mode");async function te(r,t){let e=await W(r,t.debug),o=g.create(e),s=f.create(o),a=await R.create(o).loadUseCases("INITIAL"),p=await gt("Use case",t.useCase,a,{createTitle:n=>`${n.name} (${n.id})`,displayFilter:n=>n.state!=="HIDDEN"});await s.run(p)}var ht=new Qt().name("run").description("run a use case in the workspace").addArgument(D).addOption(Yt).addOption(Zt).action(te);import{Command as ee}from"commander";async function re(r){let t=await W(r),e=g.create(t);await e.syncTemplates(),await f.create(e).run("sync")}var wt=new ee().name("sync").description("syncs the workspace templates with the package & configured repository").addArgument(D).action(re);var Ct=()=>process.exit(0);process.on("SIGINT",Ct);process.on("SIGTERM",Ct);process.on("uncaughtException",r=>{c.error(r.message),process.exit(1)});new oe().name(U.name).description(U.description).version(U.version).addCommand(dt).addCommand(ht).addCommand(wt).parseAsync().catch(r=>{c.error(r.message),process.exit(1)}).finally(()=>c.success("Finished execution.")); diff --git a/dist/templates/config/use-cases/check-requirements.json b/dist/templates/config/use-cases/check-requirements.json index 295d663..383e58b 100644 --- a/dist/templates/config/use-cases/check-requirements.json +++ b/dist/templates/config/use-cases/check-requirements.json @@ -4,6 +4,11 @@ "name": "Check requirements", "description": "This use case checks the requirements to develop locally.", "steps": [ + { + "type": "FORMULA", + "description": "Print OS info", + "formula": "console.log(`Running on ${OS}`)" + }, { "type": "COMMAND", "description": "Execute commands", diff --git a/src/services/repositories.repository.test.ts b/src/services/repositories.repository.test.ts index 12ea033..37405df 100644 --- a/src/services/repositories.repository.test.ts +++ b/src/services/repositories.repository.test.ts @@ -13,9 +13,9 @@ describe('RepositoriesRepository', () => { let sut: RepositoriesRepository; beforeEach(() => { - const config = initConfig(path, 'acme', null); + const config = initConfig(path, 'acme', null).store; - sut = RepositoriesRepository.create(config, TemplatesAccess.create(config)); + sut = RepositoriesRepository.create(TemplatesAccess.create(config)); }); it('should load repositories; empty list', async () => { diff --git a/src/services/servers.repository.test.ts b/src/services/servers.repository.test.ts index 742d968..a5742bf 100644 --- a/src/services/servers.repository.test.ts +++ b/src/services/servers.repository.test.ts @@ -13,7 +13,7 @@ describe('ServersRepository', () => { let sut: ServersRepository; beforeEach(() => { - const config = initConfig(path, 'acme', null); + const config = initConfig(path, 'acme', null).store; sut = ServersRepository.create(TemplatesAccess.create(config)); }); diff --git a/src/services/use-cases.repository.test.ts b/src/services/use-cases.repository.test.ts index 5a6f1f4..a4f04b4 100644 --- a/src/services/use-cases.repository.test.ts +++ b/src/services/use-cases.repository.test.ts @@ -13,7 +13,7 @@ describe('UseCasesRepository', () => { let sut: UseCasesRepository; beforeEach(() => { - const config = initConfig(path, 'acme', null); + const config = initConfig(path, 'acme', null).store; sut = UseCasesRepository.create(TemplatesAccess.create(config)); }); diff --git a/src/services/use-cases/context-creator.ts b/src/services/use-cases/context-creator.ts index 3c09384..e1ee99a 100644 --- a/src/services/use-cases/context-creator.ts +++ b/src/services/use-cases/context-creator.ts @@ -1,4 +1,5 @@ import { Context } from '../../types/index.js'; +import { OS } from '../../utils/index.js'; import { TemplatesAccess } from '../access/index.js'; import { RepositoriesRepository } from '../repositories.repository.js'; import { ServersRepository } from '../servers.repository.js'; @@ -25,6 +26,7 @@ export class ContextCreator { WORKING_DIR: this.templatesAccess.getWorkingDir(), SERVERS: await this.serversRepository.loadServers(), REPOSITORIES: await this.repositoriesRepository.loadRepositories(), + OS, }; } } diff --git a/src/services/use-cases/use-case-runner.test.ts b/src/services/use-cases/use-case-runner.test.ts index 7ed60d7..785054b 100644 --- a/src/services/use-cases/use-case-runner.test.ts +++ b/src/services/use-cases/use-case-runner.test.ts @@ -16,14 +16,10 @@ describe('UseCaseRunner', () => { let sut: UseCaseRunner; beforeEach(() => { - const config = initConfig(path, 'acme', null); + const config = initConfig(path, 'acme', null).store; scriptExecutor = ScriptExecutor.create(); - sut = UseCaseRunner.create( - config, - TemplatesAccess.create(config), - scriptExecutor - ); + sut = UseCaseRunner.create(TemplatesAccess.create(config), scriptExecutor); }); afterEach(() => { diff --git a/src/utils/processes.ts b/src/utils/processes.ts index 8dcaa11..132adbc 100644 --- a/src/utils/processes.ts +++ b/src/utils/processes.ts @@ -1,5 +1,7 @@ import { execSync } from 'node:child_process'; +export const OS = process.platform; + export async function execCommand(command: string, cwd: string) { return execSync(command, { cwd,