@gnar-engine/cli 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/bootstrap/deploy.localdev.yml +44 -3
  2. package/bootstrap/secrets.localdev.yml +20 -5
  3. package/bootstrap/services/control/src/config.js +4 -0
  4. package/bootstrap/services/notification/Dockerfile +2 -2
  5. package/bootstrap/services/notification/package.json +14 -32
  6. package/bootstrap/services/notification/src/app.js +50 -48
  7. package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
  8. package/bootstrap/services/notification/src/config.js +55 -12
  9. package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
  10. package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
  11. package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
  12. package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
  13. package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
  14. package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
  15. package/bootstrap/services/notification/src/services/notification.service.js +32 -0
  16. package/bootstrap/services/page/Dockerfile +23 -0
  17. package/bootstrap/services/page/package.json +16 -0
  18. package/bootstrap/services/page/src/app.js +50 -0
  19. package/bootstrap/services/page/src/commands/block.handler.js +94 -0
  20. package/bootstrap/services/page/src/commands/page.handler.js +167 -0
  21. package/bootstrap/services/page/src/config.js +62 -0
  22. package/bootstrap/services/page/src/controllers/block.http.controller.js +87 -0
  23. package/bootstrap/services/page/src/controllers/message.controller.js +51 -0
  24. package/bootstrap/services/page/src/controllers/page.http.controller.js +89 -0
  25. package/bootstrap/services/page/src/policies/block.policy.js +50 -0
  26. package/bootstrap/services/page/src/policies/page.policy.js +49 -0
  27. package/bootstrap/services/page/src/schema/page.schema.js +139 -0
  28. package/bootstrap/services/page/src/services/block.service.js +83 -0
  29. package/bootstrap/services/page/src/services/page.service.js +83 -0
  30. package/bootstrap/services/portal/Dockerfile +20 -0
  31. package/bootstrap/services/portal/README.md +73 -0
  32. package/bootstrap/services/portal/index.html +13 -0
  33. package/bootstrap/services/portal/nginx.conf +5 -0
  34. package/bootstrap/services/portal/package.json +33 -0
  35. package/bootstrap/services/portal/public/vite.svg +1 -0
  36. package/bootstrap/services/portal/react-router.config.js +7 -0
  37. package/bootstrap/services/portal/src/App.jsx +16 -0
  38. package/bootstrap/services/portal/src/assets/gnar-engine-white-logo.svg +9 -0
  39. package/bootstrap/services/portal/src/assets/icon-agent.svg +6 -0
  40. package/bootstrap/services/portal/src/assets/icon-cog.svg +4 -0
  41. package/bootstrap/services/portal/src/assets/icon-delete.svg +3 -0
  42. package/bootstrap/services/portal/src/assets/icon-home.svg +3 -0
  43. package/bootstrap/services/portal/src/assets/icon-padlock.svg +3 -0
  44. package/bootstrap/services/portal/src/assets/icon-page.svg +6 -0
  45. package/bootstrap/services/portal/src/assets/icon-reports.svg +3 -0
  46. package/bootstrap/services/portal/src/assets/icon-user.svg +3 -0
  47. package/bootstrap/services/portal/src/assets/icon-users.svg +3 -0
  48. package/bootstrap/services/portal/src/assets/login-green-rad-back-1.jpg +0 -0
  49. package/bootstrap/services/portal/src/assets/react.svg +1 -0
  50. package/bootstrap/services/portal/src/components/CrudList/CrudList.jsx +85 -0
  51. package/bootstrap/services/portal/src/components/CrudList/CrudList.less +59 -0
  52. package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.jsx +81 -0
  53. package/bootstrap/services/portal/src/components/LoginForm/LoginForm.jsx +58 -0
  54. package/bootstrap/services/portal/src/components/PageBlockSwitch/PageBlockSwitch.jsx +129 -0
  55. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.jsx +33 -0
  56. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.less +37 -0
  57. package/bootstrap/services/portal/src/components/Topbar/Topbar.jsx +19 -0
  58. package/bootstrap/services/portal/src/components/Topbar/Topbar.less +22 -0
  59. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.jsx +33 -0
  60. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.less +21 -0
  61. package/bootstrap/services/portal/src/css/style.css +711 -0
  62. package/bootstrap/services/portal/src/data/pages.data.js +10 -0
  63. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.jsx +65 -0
  64. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.less +102 -0
  65. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.jsx +115 -0
  66. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.less +43 -0
  67. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.jsx +124 -0
  68. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.less +0 -0
  69. package/bootstrap/services/portal/src/elements/Repeater/Repeater.jsx +52 -0
  70. package/bootstrap/services/portal/src/elements/Repeater/Repeater.less +70 -0
  71. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.jsx +18 -0
  72. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.less +37 -0
  73. package/bootstrap/services/portal/src/elements/SaveButton/SaveButton.jsx +45 -0
  74. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.jsx +63 -0
  75. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.less +23 -0
  76. package/bootstrap/services/portal/src/elements/TextInput/TextInput.jsx +17 -0
  77. package/bootstrap/services/portal/src/layouts/Card/Card.jsx +15 -0
  78. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.jsx +29 -0
  79. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.less +49 -0
  80. package/bootstrap/services/portal/src/main.jsx +51 -0
  81. package/bootstrap/services/portal/src/pages/BlockSinglePage/BlockSinglePage.jsx +277 -0
  82. package/bootstrap/services/portal/src/pages/BlocksPage/BlocksPage.jsx +23 -0
  83. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.jsx +11 -0
  84. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.less +0 -0
  85. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.jsx +21 -0
  86. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.less +51 -0
  87. package/bootstrap/services/portal/src/pages/PageSinglePage/PageSinglePage.jsx +338 -0
  88. package/bootstrap/services/portal/src/pages/PagesPage/PagesPage.jsx +23 -0
  89. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.jsx +9 -0
  90. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.less +0 -0
  91. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.jsx +25 -0
  92. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.less +0 -0
  93. package/bootstrap/services/portal/src/services/block.js +28 -0
  94. package/bootstrap/services/portal/src/services/client.js +70 -0
  95. package/bootstrap/services/portal/src/services/gravatar.js +14 -0
  96. package/bootstrap/services/portal/src/services/page.js +28 -0
  97. package/bootstrap/services/portal/src/services/storage.js +62 -0
  98. package/bootstrap/services/portal/src/services/user.js +41 -0
  99. package/bootstrap/services/portal/src/slices/authSlice.js +101 -0
  100. package/bootstrap/services/portal/src/store/configureStore.js +10 -0
  101. package/bootstrap/services/portal/src/style/cards.less +57 -0
  102. package/bootstrap/services/portal/src/style/global.less +204 -0
  103. package/bootstrap/services/portal/src/style/icons.less +21 -0
  104. package/bootstrap/services/portal/src/style/inputs.less +52 -0
  105. package/bootstrap/services/portal/src/style/main.less +28 -0
  106. package/bootstrap/services/portal/src/utils/utils.js +9 -0
  107. package/bootstrap/services/portal/vite.config.js +12 -0
  108. package/bootstrap/services/user/src/app.js +6 -1
  109. package/bootstrap/services/user/src/commands/user.handler.js +35 -21
  110. package/bootstrap/services/user/src/config.js +5 -1
  111. package/bootstrap/services/user/src/policies/user.policy.js +3 -1
  112. package/bootstrap/services/user/src/tests/commands/user.test.js +31 -0
  113. package/install-from-clone.sh +30 -0
  114. package/package.json +1 -1
  115. package/src/cli.js +2 -0
  116. package/src/config.js +8 -0
  117. package/src/dev/commands.js +11 -3
  118. package/src/dev/dev.service.js +164 -64
  119. package/src/helpers/helpers.js +24 -0
  120. package/src/profiles/command.js +41 -0
  121. package/src/profiles/profiles.client.js +23 -0
  122. package/src/provisioner/Dockerfile +27 -0
  123. package/src/provisioner/package.json +19 -0
  124. package/src/provisioner/src/app.js +56 -0
  125. package/src/provisioner/src/services/mongodb.js +58 -0
  126. package/src/provisioner/src/services/mysql.js +51 -0
  127. package/src/provisioner/src/services/secrets.js +84 -0
  128. package/src/scaffolder/commands.js +58 -2
  129. package/src/scaffolder/scaffolder.handler.js +164 -72
  130. package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
  131. package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
  132. package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
  133. package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
  134. package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
  135. package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
  136. package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
  137. package/templates/service/src/app.js.hbs +12 -1
  138. package/templates/service/src/commands/{{serviceName}}.handler.js.hbs +1 -1
  139. package/templates/service/src/mongodb.config.js.hbs +5 -1
  140. package/templates/service/src/mysql.config.js.hbs +4 -0
  141. package/bootstrap/services/notification/Dockerfile.prod +0 -37
  142. package/bootstrap/services/notification/README.md +0 -3
  143. package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
  144. package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
  145. package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
  146. package/bootstrap/services/notification/src/services/logger.service.js +0 -16
  147. package/bootstrap/services/notification/src/services/ses.service.js +0 -23
  148. package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
  149. package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
  150. package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
  151. package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
  152. package/bootstrap/services/user/src/tests/user.test.js +0 -126
  153. /package/bootstrap/services/{notification/src/tests/notification.test.js → portal/src/components/CustomSelect/CustomSelect.less} +0 -0
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ TARGET_DIR="$HOME/.gnarengine"
5
+ CLI_DIR="$(pwd)"
6
+
7
+ echo "Installing Gnar Engine CLI from local source for user $USER..."
8
+
9
+ mkdir -p "$TARGET_DIR"
10
+
11
+ # Install dependencies
12
+ cd "$CLI_DIR"
13
+ npm install
14
+
15
+ # Link CLI to custom global folder
16
+ npm link
17
+
18
+ # Bin path for npm link
19
+ BIN_PATH="$TARGET_DIR/bin"
20
+
21
+ # Add to shell PATH if not already present
22
+ for SHELLRC in "$HOME/.bashrc" "$HOME/.zshrc"; do
23
+ [ -f "$SHELLRC" ] || continue
24
+ if ! grep -q "$BIN_PATH" "$SHELLRC"; then
25
+ echo "export PATH=\"$BIN_PATH:\$PATH\"" >>"$SHELLRC"
26
+ echo "Added $BIN_PATH to PATH in $SHELLRC"
27
+ fi
28
+ done
29
+
30
+ echo "Gnar Engine CLI installed! Restart your terminal and run 'gnar --help'"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnar-engine/cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Gnar Engine Development Framework CLI: Project bootstrap, scaffolder & control plane.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -6,6 +6,7 @@ import { registerDevCommands } from './dev/commands.js';
6
6
  import { registerControlCommands } from './control/commands.js';
7
7
  import { registerScaffolderCommands } from './scaffolder/commands.js';
8
8
  import { registerAgentCommands } from './agent/commands.js';
9
+ import path from 'path';
9
10
 
10
11
  // Create a new program
11
12
  const program = new Command();
@@ -28,3 +29,4 @@ G n a r E n g i n e - A powerful, AI ready microservice framework for modern a
28
29
 
29
30
  // Parse CLI input
30
31
  program.parse(process.argv);
32
+
package/src/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
 
2
3
  export const gnarEngineCliConfig = {
3
4
 
@@ -7,3 +8,10 @@ export const gnarEngineCliConfig = {
7
8
  corePath: '/usr/gnar_engine/app/node_modules/@gnar-engine/core'
8
9
 
9
10
  }
11
+
12
+ export const directories = {
13
+ scaffolderServiceTemplates: path.join(import.meta.dirname, '../templates/service'),
14
+ scaffolderEntityTemplates: path.join(import.meta.dirname, '../templates/entity'),
15
+ bootstrap: path.join(import.meta.dirname, '../bootstrap'),
16
+ provisioner: path.join(import.meta.dirname, './provisioner')
17
+ };
@@ -10,8 +10,10 @@ export function registerDevCommands(program) {
10
10
  devCmd
11
11
  .command('up')
12
12
  .description('🛠️ Up Development Containers')
13
- .option('-b, --build', 'Ruild without cache')
13
+ .option('-b, --build', 'Build without cache')
14
14
  .option('-d, --detach', 'Run containers in background')
15
+ .option('-t --test', 'Run the tests with ephemeral databases')
16
+ .option('--test-service <service>', 'Run the tests for the specified service with ephemeral databases (e.g. --test-service user)')
15
17
  .addOption(new Option('--core-dev').hideHelp())
16
18
  .action(async (options) => {
17
19
  let response = {};
@@ -27,13 +29,19 @@ export function registerDevCommands(program) {
27
29
  // Change to the active profile directory
28
30
  const projectDir = activeProfile.PROJECT_DIR;
29
31
 
32
+ if (options.testService) {
33
+ options.test = true;
34
+ }
35
+
30
36
  try {
31
37
  up({
32
38
  projectDir: projectDir,
33
39
  build: options.build || false,
34
40
  detach: options.detach || false,
35
- coreDev: options.coreDev || false
36
- });
41
+ coreDev: options.coreDev || false,
42
+ test: options.test || false,
43
+ testService: options.testService || ''
44
+ });
37
45
  } catch (err) {
38
46
  console.error("❌ Error running containers:", err.message);
39
47
  process.exit(1);
@@ -5,6 +5,7 @@ import fs from "fs/promises";
5
5
  import path from "path";
6
6
  import yaml from "js-yaml";
7
7
  import { gnarEngineCliConfig } from "../config.js";
8
+ import { directories } from "../config.js";
8
9
 
9
10
  const docker = new Docker();
10
11
 
@@ -16,10 +17,13 @@ const docker = new Docker();
16
17
  * @param {object} options
17
18
  * @param {string} options.projectDir - The project directory
18
19
  * @param {boolean} [options.build=false] - Whether to re-build images
19
- * @param {boolean} [options.detached=false] - Whether to run containers in background
20
+ * @param {boolean} [options.detach=false] - Whether to run containers in background
20
21
  * @param {boolean} [options.coreDev=false] - Whether to run in core development mode (requires access to core source)
22
+ * @param {boolean} [options.test=false] - Whether to run tests with ephemeral databases
23
+ * @param {string} [options.testService=''] - The service to run tests for (only applicable if test=true)
24
+ * @param {boolean} [options.removeOrphans=true] - Whether to remove orphaned containers
21
25
  */
22
- export async function up({ projectDir, build = false, detached = false, coreDev = false }) {
26
+ export async function up({ projectDir, build = false, detach = false, coreDev = false, test = false, testService = '', removeOrphans = true }) {
23
27
 
24
28
  // core dev
25
29
  if (coreDev) {
@@ -40,8 +44,13 @@ export async function up({ projectDir, build = false, detached = false, coreDev
40
44
 
41
45
  // create nginx.conf dynamically from configPath
42
46
  const nginxConfPath = path.join(gnarHiddenDir, "nginx", "nginx.conf");
47
+ const serviceConfDir = path.join(gnarHiddenDir, "nginx", "service_conf")
48
+ await fs.mkdir(serviceConfDir, { recursive: true });
49
+
43
50
  const nginxConf = await createDynamicNginxConf({
44
- config: parsedConfig.config
51
+ config: parsedConfig.config,
52
+ projectDir: projectDir,
53
+ serviceConfDir: serviceConfDir
45
54
  });
46
55
  await fs.writeFile(nginxConfPath, nginxConf);
47
56
 
@@ -52,7 +61,9 @@ export async function up({ projectDir, build = false, detached = false, coreDev
52
61
  secrets: parsedSecrets,
53
62
  gnarHiddenDir: gnarHiddenDir,
54
63
  projectDir: projectDir,
55
- coreDev: coreDev
64
+ coreDev: coreDev,
65
+ test: test,
66
+ testService: testService
56
67
  });
57
68
  await fs.writeFile(dockerComposePath, yaml.dump(dockerCompose));
58
69
 
@@ -63,10 +74,14 @@ export async function up({ projectDir, build = false, detached = false, coreDev
63
74
  args.push("--build");
64
75
  }
65
76
 
66
- if (detached) {
77
+ if (detach) {
67
78
  args.push("-d");
68
79
  }
69
80
 
81
+ if (removeOrphans) {
82
+ args.push("--remove-orphans")
83
+ }
84
+
70
85
  const processRef = spawn(
71
86
  "docker-compose",
72
87
  args,
@@ -96,11 +111,11 @@ export async function up({ projectDir, build = false, detached = false, coreDev
96
111
  */
97
112
  export async function down({ projectDir, allContainers = false }) {
98
113
  // list all containers
99
- const containers = await docker.listContainers();
114
+ let containers = await docker.listContainers();
100
115
 
101
116
  // filter containers by image name
102
117
  if (!allContainers) {
103
- const containers = containers.filter(c => c.Image.includes("ge-dev"));
118
+ containers = containers.filter(c => c.Image.includes("ge-localdev"));
104
119
  }
105
120
 
106
121
  if (containers.length === 0) {
@@ -122,15 +137,28 @@ export async function down({ projectDir, allContainers = false }) {
122
137
  });
123
138
  })
124
139
  );
140
+
141
+ // remove each container
142
+ await Promise.all(
143
+ containers.map(c => {
144
+ const container = docker.getContainer(c.Id);
145
+ return container.remove({ force: true }).catch(err => {
146
+ console.error(`Failed to remove ${c.Names[0]}: ${err.message}`);
147
+ });
148
+ })
149
+ );
150
+
151
+ console.log('Containers stopped and removed.');
125
152
  }
126
153
 
127
154
  /**
128
155
  * Create dynamic nginx.conf file for running application locally
129
156
  *
130
157
  * @param {object} config
131
- * @param {string} outputPath - where to write nginx.conf
158
+ * @param {string} serviceConfDir
159
+ * @param {string} projectDir
132
160
  */
133
- export async function createDynamicNginxConf({ config, outputPath }) {
161
+ export async function createDynamicNginxConf({ config, serviceConfDir, projectDir }) {
134
162
  // Start with the static parts of nginx.conf
135
163
  let nginxConf = `
136
164
  events { worker_connections 1024; }
@@ -139,10 +167,29 @@ export async function createDynamicNginxConf({ config, outputPath }) {
139
167
  server {
140
168
  listen 80;
141
169
  server_name ${config.namespace};
170
+ include /etc/nginx/service_conf/*.conf;
171
+
142
172
  `;
143
173
 
144
174
  // Loop over each service
145
175
  for (const service of config.services || []) {
176
+ // Check if override is present and add conf to service_conf dir
177
+ const serviceDir = path.join(projectDir, 'services', service.name);
178
+
179
+ if (await fs.stat(serviceDir).then(() => true).catch(() => false)) {
180
+ const overridePath = path.join(serviceDir, 'nginx.conf');
181
+ if (await fs.stat(overridePath).then(() => true).catch(() => false)) {
182
+ const overrideConf = await fs.readFile(overridePath, 'utf8');
183
+
184
+ // write to service_conf directory
185
+ const serviceConfPath = path.join(serviceConfDir, `${service.name}.conf`);
186
+ await fs.writeFile(serviceConfPath, overrideConf);
187
+
188
+ continue;
189
+ }
190
+ }
191
+
192
+ // Otherwise create generic conf block
146
193
  const serviceName = service.name;
147
194
  const paths = service.listener_rules?.paths || [];
148
195
  const containerPort = service.ports && service.ports.length > 0 ? service.ports[0].split(':')[1] : '3000';
@@ -179,12 +226,57 @@ export async function createDynamicNginxConf({ config, outputPath }) {
179
226
  * @param {string} gnarHiddenDir
180
227
  * @param {string} projectDir
181
228
  * @param {boolean} coreDev - Whether to volume mount the core source code
229
+ * @param {boolean} test - Whether to run tests with ephemeral databases
230
+ * @param {string} testService - The service to run tests for (only applicable if test=true)
231
+ * @param {boolean} attachAll - Whether to attach to all containers' stdio (otherwise databases and message queue are detached)
182
232
  */
183
- async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false }) {
233
+ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, test = false, testService, attachAll = false }) {
184
234
  let mysqlPortsCounter = 3306;
185
235
  let mongoPortsCounter = 27017;
236
+ let mysqlHostsRequired = [];
237
+ let mongoHostsRequired = [];
186
238
  const services = {};
187
-
239
+
240
+ // test mode env var adjustments
241
+ for (const svc of config.services) {
242
+ if (test) {
243
+ if (secrets.services?.[svc.name]?.MYSQL_HOST) {
244
+ secrets.services[svc.name].MYSQL_HOST = 'db-mysql-test';
245
+ }
246
+
247
+ if (secrets.services?.[svc.name]) {
248
+ secrets.services[svc.name].NODE_ENV = 'test';
249
+
250
+ console.log(testService, svc.name);
251
+ if (testService && svc.name === testService) {
252
+ secrets.services[svc.name].RUN_TESTS = 'true';
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ // provision the provisioner service
259
+ services['provisioner'] = {
260
+ container_name: `ge-${config.environment}-${config.namespace}-provisioner`,
261
+ image: `ge-${config.environment}-${config.namespace}-provisioner`,
262
+ build: {
263
+ context: directories.provisioner,
264
+ dockerfile: `./Dockerfile`
265
+ },
266
+ environment: {
267
+ PROVISIONER_SECRETS: JSON.stringify(secrets)
268
+ },
269
+ volumes: [
270
+ `${directories.provisioner}/src:/usr/gnar_engine/app/src`
271
+ ],
272
+ restart: 'no',
273
+ attach: attachAll
274
+ }
275
+
276
+ if (coreDev) {
277
+ services['provisioner'].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
278
+ }
279
+
188
280
  // nginx
189
281
  services['nginx'] = {
190
282
  image: 'nginx:latest',
@@ -194,9 +286,11 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
194
286
  "443:443"
195
287
  ],
196
288
  volumes: [
197
- `${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`
289
+ `${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`,
290
+ `${gnarHiddenDir}/nginx/service_conf:/etc/nginx/service_conf`
198
291
  ],
199
- restart: 'always'
292
+ restart: 'always',
293
+ attach: attachAll
200
294
  }
201
295
 
202
296
  // rabbit
@@ -211,7 +305,8 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
211
305
  RABBITMQ_DEFAULT_USER: secrets.global.RABBITMQ_USER || '',
212
306
  RABBITMQ_DEFAULT_PASS: secrets.global.RABBITMQ_PASS || ''
213
307
  },
214
- restart: 'always'
308
+ restart: 'always',
309
+ attach: attachAll
215
310
  }
216
311
 
217
312
  // services
@@ -220,7 +315,7 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
220
315
  // env variables
221
316
  const serviceEnvVars = secrets.services?.[svc.name] || {};
222
317
  const localisedServiceEnvVars = {};
223
-
318
+
224
319
  for (const [key, value] of Object.entries(serviceEnvVars)) {
225
320
  localisedServiceEnvVars[svc.name.toUpperCase() + '_' + key] = value;
226
321
  }
@@ -230,6 +325,14 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
230
325
  ...(localisedServiceEnvVars || {})
231
326
  };
232
327
 
328
+ // test mode adjustments
329
+ if (test) {
330
+ if (svc.depends_on && svc.depends_on.includes('db-mysql')) {
331
+ svc.depends_on = svc.depends_on.filter(d => d !== 'db-mysql');
332
+ svc.depends_on.push('db-mysql-test');
333
+ }
334
+ }
335
+
233
336
  // service block
234
337
  services[`${svc.name}-service`] = {
235
338
  container_name: `ge-${config.environment}-${config.namespace}-${svc.name}`,
@@ -248,69 +351,67 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
248
351
  restart: 'always'
249
352
  };
250
353
 
251
- // add the core source code mount if in coreDeve mode
354
+ // add the core source code mount if in coreDev mode
252
355
  if (coreDev) {
253
356
  services[`${svc.name}-service`].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
254
357
  }
255
358
 
256
- // add a mysql instance if required
359
+ // check if mysql service required
257
360
  if (
258
361
  serviceEnvVars.MYSQL_HOST &&
259
- serviceEnvVars.MYSQL_DATABASE &&
260
- serviceEnvVars.MYSQL_USER &&
261
- serviceEnvVars.MYSQL_PASSWORD &&
262
- serviceEnvVars.MYSQL_RANDOM_ROOT_PASSWORD
362
+ secrets.provision?.MYSQL_ROOT_PASSWORD
363
+ ) {
364
+ mysqlHostsRequired.push(serviceEnvVars.MYSQL_HOST);
365
+ }
366
+
367
+ // add a mongodb instance if required
368
+ if (
369
+ serviceEnvVars.MONGO_HOST &&
370
+ secrets.provision?.MONGO_ROOT_PASSWORD
263
371
  ) {
264
- services[`${svc.name}-db`] = {
265
- container_name: `ge-${config.environment}-${config.namespace}-${svc.name}-db`,
372
+ mongoHostsRequired.push(serviceEnvVars.MONGO_HOST);
373
+ }
374
+ }
375
+
376
+ // add mysql if required
377
+ if (mysqlHostsRequired.length > 0) {
378
+ for (const host of mysqlHostsRequired) {
379
+ if (services[host]) {
380
+ continue;
381
+ }
382
+
383
+ services[host] = {
384
+ container_name: `ge-${config.environment}-${config.namespace}-${host}`,
266
385
  image: 'mysql',
267
386
  ports: [
268
387
  `${mysqlPortsCounter}:${mysqlPortsCounter}`
269
388
  ],
270
389
  restart: 'always',
271
390
  environment: {
272
- MYSQL_HOST: serviceEnvVars.MYSQL_HOST,
273
- MYSQL_DATABASE: serviceEnvVars.MYSQL_DATABASE,
274
- MYSQL_USER: serviceEnvVars.MYSQL_USER,
275
- MYSQL_PASSWORD: serviceEnvVars.MYSQL_PASSWORD,
276
- MYSQL_RANDOM_ROOT_PASSWORD: serviceEnvVars.MYSQL_RANDOM_ROOT_PASSWORD,
391
+ MYSQL_HOST: host,
392
+ MYSQL_ROOT_PASSWORD: secrets.provision.MYSQL_ROOT_PASSWORD
277
393
  },
278
394
  volumes: [
279
- `${gnarHiddenDir}/data/${svc.name}-db-data:/var/lib/mysql`
280
- ]
281
- };
395
+ `${gnarHiddenDir}/data/${host}-data:/var/lib/mysql`
396
+ ],
397
+ attach: attachAll
398
+ };
282
399
 
283
- // increment mysql port for next service as required
284
400
  mysqlPortsCounter++;
285
401
  }
286
402
 
287
- // add a mongodb instance if required
288
- if (
289
- serviceEnvVars.MONGO_HOST &&
290
- serviceEnvVars.MONGO_ROOT_PASSWORD &&
291
- serviceEnvVars.MONGO_DATABASE &&
292
- serviceEnvVars.MONGO_USER &&
293
- serviceEnvVars.MONGO_PASSWORD
294
- ) {
295
- // add mongo init scripts to hidden dir
296
- fs.mkdir(path.join(gnarHiddenDir, 'mongo-init-scripts'), { recursive: true });
297
- const mongoInitScript = `
298
- db = db.getSiblingDB("invoice_db");
299
- db.createUser({
300
- user: "${serviceEnvVars.MONGO_USER}",
301
- pwd: "${serviceEnvVars.MONGO_PASSWORD}",
302
- roles: [{ role: "readWrite", db: "${serviceEnvVars.MONGO_DATABASE}" }]
303
- });
304
-
305
- print("Created user ${serviceEnvVars.MONGO_USER} with access to database ${serviceEnvVars.MONGO_DATABASE}");
306
- `;
307
- await fs.writeFile(path.join(gnarHiddenDir, 'mongo-init-scripts', `${svc.name}-init.js`), mongoInitScript);
403
+ services['provisioner'].depends_on = [...new Set(mysqlHostsRequired)];
404
+ }
405
+
406
+ // add mongo hosts if required
407
+ if (mongoHostsRequired.length > 0) {
408
+ for (const host of mongoHostsRequired) {
409
+ if (services[host]) {
410
+ continue;
411
+ }
308
412
 
309
- // create mongo service
310
- const mongoUrl = `mongodb://${serviceEnvVars.MONGO_USER}:${serviceEnvVars.MONGO_PASSWORD}@${serviceEnvVars.MONGO_HOST}:27017/${serviceEnvVars.MONGO_DATABASE}`;
311
-
312
- services[`${svc.name}-db`] = {
313
- container_name: `ge-${config.environment}-${config.namespace}-${svc.name}-mongo`,
413
+ services[host] = {
414
+ container_name: `ge-${config.environment}-${config.namespace}-${host}`,
314
415
  image: 'mongo:latest',
315
416
  ports: [
316
417
  `${mongoPortsCounter}:27017`
@@ -318,15 +419,13 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
318
419
  restart: 'always',
319
420
  environment: {
320
421
  MONGO_INITDB_ROOT_USERNAME: 'root',
321
- MONGO_INITDB_ROOT_PASSWORD: serviceEnvVars.MONGO_ROOT_PASSWORD,
322
- MONGO_INITDB_DATABASE: serviceEnvVars.MONGO_DATABASE,
323
- DB_USER: serviceEnvVars.MONGO_USER,
324
- DB_PASSWORD: serviceEnvVars.MONGO_PASSWORD,
422
+ MONGO_INITDB_ROOT_PASSWORD: secrets.provision.MONGO_ROOT_PASSWORD
325
423
  },
326
424
  volumes: [
327
- `${gnarHiddenDir}/data/${svc.name}-mongo-data:/data/db`,
425
+ `${gnarHiddenDir}/data/${host}-data:/data/db`,
328
426
  './mongo-init-scripts:/docker-entrypoint-initdb.d'
329
- ]
427
+ ],
428
+ attach: attachAll
330
429
  };
331
430
 
332
431
  // increment mongo port for next service as required
@@ -346,3 +445,4 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
346
445
  async function assertGnarEngineHiddenDir(gnarHiddenDir) {
347
446
  await fs.mkdir(gnarHiddenDir, { recursive: true });
348
447
  }
448
+
@@ -1,3 +1,6 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
1
4
 
2
5
  /**
3
6
  * CLI helper functions
@@ -59,5 +62,26 @@ export const helpers = {
59
62
  result += chars.charAt(Math.floor(Math.random() * chars.length));
60
63
  }
61
64
  return result;
65
+ },
66
+
67
+ getDbTypeFromSecrets: async (serviceName, projectDir) => {
68
+ let dbType;
69
+ const secretsPath = path.join(projectDir, "secrets.localdev.yml");
70
+ const parsedSecrets = yaml.load(await fs.readFile(secretsPath, "utf8"));
71
+ const serviceSecrets = parsedSecrets.services[serviceName.toLowerCase()];
72
+
73
+ Object.keys(serviceSecrets).forEach(key => {
74
+ if (key.toLowerCase().includes('host')) {
75
+ const host = serviceSecrets[key].toLowerCase();
76
+
77
+ if (host.includes('mongo')) {
78
+ dbType = 'mongodb';
79
+ } else if (host.includes('mysql')) {
80
+ dbType = 'mysql';
81
+ }
82
+ }
83
+ });
84
+
85
+ return dbType;
62
86
  }
63
87
  }
@@ -166,5 +166,46 @@ export function registerProfileCommand(program) {
166
166
  });
167
167
  });
168
168
 
169
+ // delete profile
170
+ profile
171
+ .command('delete <profileName>')
172
+ .description('Delete an existing profile')
173
+ .action(async (profileName) => {
174
+ const config = profiles.getAllProfiles();
175
+ const activeProfileName = config.activeProfile;
176
+
177
+ if (activeProfileName === profileName) {
178
+ console.error(`Cannot delete active profile "${profileName}". Please set another profile as active first.`);
179
+ return;
180
+ }
181
+
182
+ if (!config.profiles[profileName]) {
183
+ console.error(`Profile "${profileName}" not found.`);
184
+ return;
185
+ }
186
+
187
+ try {
188
+ // confirm deletion with user
189
+ const confirmation = await inquirer.prompt([
190
+ {
191
+ type: 'confirm',
192
+ name: 'confirmDelete',
193
+ message: `Are you sure you want to delete profile "${profileName}"?`,
194
+ default: false,
195
+ },
196
+ ]);
197
+
198
+ if (!confirmation.confirmDelete) {
199
+ console.log('❌ Deletion cancelled.');
200
+ return;
201
+ }
202
+
203
+ profiles.deleteProfile({ profileName });
204
+ console.log(`✅ Profile "${profileName}" deleted successfully.`);
205
+ } catch (error) {
206
+ console.error(error.message);
207
+ }
208
+ });
209
+
169
210
  program.addCommand(profile);
170
211
  }
@@ -90,6 +90,29 @@ export const profiles = {
90
90
  this.saveProfiles(allProfiles);
91
91
  },
92
92
 
93
+ deleteProfile: function ({ profileName }) {
94
+ if (!profileName) {
95
+ throw new Error('Invalid profile name');
96
+ }
97
+
98
+ const allProfiles = this.getAllProfiles().profiles || {};
99
+
100
+ if (!allProfiles[profileName]) {
101
+ throw new Error(`Profile "${profileName}" not found`);
102
+ }
103
+
104
+ const activeProfileName = this.getActiveProfile()?.name;
105
+
106
+ if (activeProfileName === profileName) {
107
+ throw new Error(`Cannot delete active profile "${profileName}". Please set another profile as active first.`);
108
+ }
109
+
110
+ // Prompt user to confirm deletion in the console
111
+ delete allProfiles[profileName];
112
+
113
+ this.saveProfiles(allProfiles);
114
+ },
115
+
93
116
  saveProfiles: function (profilesObj) {
94
117
  const dir = path.dirname(this.configPath);
95
118
  if (!fs.existsSync(dir)) {
@@ -0,0 +1,27 @@
1
+ # Dockerfile for the ephemeral provisioning service
2
+ #
3
+ # This service is responsible for asserting the databases
4
+ # & database users, or other single runtime provisioning tasks.
5
+
6
+ FROM node:20-alpine
7
+
8
+ # Set the working directory
9
+ WORKDIR /usr/gnar_engine/app
10
+
11
+ # Define a global env var
12
+ ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
13
+
14
+ # Copy package.json and package-lock.json
15
+ COPY ./package*.json ./
16
+
17
+ # Copy source
18
+ COPY ./src ./src
19
+
20
+ # Install nodemon
21
+ RUN npm install -g nodemon
22
+
23
+ # Install app dependencies
24
+ RUN npm install
25
+
26
+ # Start the application
27
+ CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "gnar_engine_user",
3
+ "version": "1.0.0",
4
+ "description": "Gnar Engine - User Service",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node ./src/app.js",
8
+ "start:dev": "nodemon --watch ./src ./src/app.js",
9
+ "test": "jest --watchAll --verbose"
10
+ },
11
+ "author": "Gnar Software Ltd.",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@gnar-engine/core": "^1.0.1",
15
+ "dotenv": "^16.4.7",
16
+ "mongodb": "^5.9.2",
17
+ "mysql2": "^3.12.0"
18
+ }
19
+ }