@gnar-engine/cli 1.0.5 → 1.0.7

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 (48) hide show
  1. package/bootstrap/deploy.localdev.yml +14 -0
  2. package/bootstrap/secrets.localdev.yml +7 -3
  3. package/bootstrap/services/notification/Dockerfile +2 -2
  4. package/bootstrap/services/notification/package.json +14 -32
  5. package/bootstrap/services/notification/src/app.js +50 -48
  6. package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
  7. package/bootstrap/services/notification/src/config.js +55 -12
  8. package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
  9. package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
  10. package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
  11. package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
  12. package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
  13. package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
  14. package/bootstrap/services/notification/src/services/notification.service.js +32 -0
  15. package/bootstrap/services/portal/src/services/client.js +3 -0
  16. package/bootstrap/services/user/src/commands/user.handler.js +35 -18
  17. package/bootstrap/services/user/src/tests/commands/user.test.js +15 -6
  18. package/install-from-clone.sh +1 -1
  19. package/package.json +1 -1
  20. package/src/cli.js +0 -6
  21. package/src/config.js +13 -1
  22. package/src/dev/commands.js +7 -3
  23. package/src/dev/dev.service.js +192 -128
  24. package/src/helpers/helpers.js +24 -0
  25. package/src/profiles/command.js +41 -0
  26. package/src/profiles/profiles.client.js +23 -0
  27. package/src/scaffolder/commands.js +57 -1
  28. package/src/scaffolder/scaffolder.handler.js +127 -60
  29. package/src/services/docker.js +173 -0
  30. package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
  31. package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
  32. package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
  33. package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
  34. package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
  35. package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
  36. package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
  37. package/bootstrap/services/notification/Dockerfile.prod +0 -37
  38. package/bootstrap/services/notification/README.md +0 -3
  39. package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
  40. package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
  41. package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
  42. package/bootstrap/services/notification/src/services/logger.service.js +0 -16
  43. package/bootstrap/services/notification/src/services/ses.service.js +0 -23
  44. package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
  45. package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
  46. package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
  47. package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
  48. package/bootstrap/services/notification/src/tests/notification.test.js +0 -0
@@ -3,9 +3,12 @@ import Docker from "dockerode";
3
3
  import process from "process";
4
4
  import fs from "fs/promises";
5
5
  import path from "path";
6
+ import { fileURLToPath } from 'url';
6
7
  import yaml from "js-yaml";
7
8
  import { gnarEngineCliConfig } from "../config.js";
8
- import { directories } from "../cli.js";
9
+ import { buildImage, createContainer, createBridgeNetwork } from "../services/docker.js";
10
+ import { directories } from "../config.js";
11
+
9
12
 
10
13
  const docker = new Docker();
11
14
 
@@ -17,16 +20,18 @@ const docker = new Docker();
17
20
  * @param {object} options
18
21
  * @param {string} options.projectDir - The project directory
19
22
  * @param {boolean} [options.build=false] - Whether to re-build images
20
- * @param {boolean} [options.detached=false] - Whether to run containers in background
23
+ * @param {boolean} [options.detach=false] - Whether to run containers in background
21
24
  * @param {boolean} [options.coreDev=false] - Whether to run in core development mode (requires access to core source)
25
+ * @param {boolean} [options.bootstrapDev=false] - Whether to set the cli/src/bootstrap directory as the project directory
22
26
  * @param {boolean} [options.test=false] - Whether to run tests with ephemeral databases
23
27
  * @param {string} [options.testService=''] - The service to run tests for (only applicable if test=true)
24
28
  * @param {boolean} [options.removeOrphans=true] - Whether to remove orphaned containers
29
+ * @param {boolean} [options.attachAll=false] - Attach all services including database and message queues for debugging
25
30
  */
26
- export async function up({ projectDir, build = false, detached = false, coreDev = false, test = false, testService = '', removeOrphans = true }) {
31
+ export async function up({ projectDir, build = false, detach = false, coreDev = false, bootstrapDev = false, test = false, testService = '', removeOrphans = true, attachAll = false}) {
27
32
 
28
- // core dev
29
- if (coreDev) {
33
+ // bootstrap dev
34
+ if (bootstrapDev) {
30
35
  const fileDir = path.dirname(new URL(import.meta.url).pathname);
31
36
  projectDir = path.resolve(fileDir, "../../bootstrap/");
32
37
  }
@@ -56,50 +61,51 @@ export async function up({ projectDir, build = false, detached = false, coreDev
56
61
 
57
62
  // create docker-compose.yml dynamically from parsed config and secrets
58
63
  const dockerComposePath = path.join(gnarHiddenDir, "docker-compose.dev.yml");
59
- const dockerCompose = await createDynamicDockerCompose({
64
+ const dockerCompose = await buildAndUpContainers({
60
65
  config: parsedConfig.config,
61
66
  secrets: parsedSecrets,
62
67
  gnarHiddenDir: gnarHiddenDir,
63
68
  projectDir: projectDir,
64
69
  coreDev: coreDev,
70
+ bootstrapDev: bootstrapDev,
65
71
  test: test,
66
- testService: testService
72
+ testService: testService,
73
+ attachAll: attachAll
67
74
  });
68
- await fs.writeFile(dockerComposePath, yaml.dump(dockerCompose));
69
-
70
- // up docker-compose
71
- const args = ["-f", dockerComposePath, "up"];
72
-
73
- if (build) {
74
- args.push("--build");
75
- }
76
-
77
- if (detached) {
78
- args.push("-d");
79
- }
80
-
81
- if (removeOrphans) {
82
- args.push("--remove-orphans")
83
- }
84
75
 
85
- const processRef = spawn(
86
- "docker-compose",
87
- args,
88
- {
89
- cwd: projectDir,
90
- stdio: "inherit",
91
- shell: "/bin/sh"
92
- }
93
- );
94
-
95
- // handle exit
96
- const exitCode = await new Promise((resolve) => {
97
- processRef.on("close", resolve);
98
- });
99
-
100
- if (exitCode !== 0) {
101
- throw new Error(`docker-compose up exited with code ${exitCode}`);
102
- }
76
+ // // up docker-compose
77
+ // const args = ["-f", dockerComposePath, "up"];
78
+ //
79
+ // if (build) {
80
+ // args.push("--build");
81
+ // }
82
+ //
83
+ // if (detach) {
84
+ // args.push("-d");
85
+ // }
86
+ //
87
+ // if (removeOrphans) {
88
+ // args.push("--remove-orphans")
89
+ // }
90
+
91
+ // const processRef = spawn(
92
+ // "docker-compose",
93
+ // args,
94
+ // {
95
+ // cwd: projectDir,
96
+ // stdio: "inherit",
97
+ // shell: "/bin/sh"
98
+ // }
99
+ // );
100
+ //
101
+ // // handle exit
102
+ // const exitCode = await new Promise((resolve) => {
103
+ // processRef.on("close", resolve);
104
+ // });
105
+ //
106
+ // if (exitCode !== 0) {
107
+ // throw new Error(`docker-compose up exited with code ${exitCode}`);
108
+ // }
103
109
  }
104
110
 
105
111
  /**
@@ -111,11 +117,11 @@ export async function up({ projectDir, build = false, detached = false, coreDev
111
117
  */
112
118
  export async function down({ projectDir, allContainers = false }) {
113
119
  // list all containers
114
- const containers = await docker.listContainers();
120
+ let containers = await docker.listContainers();
115
121
 
116
122
  // filter containers by image name
117
123
  if (!allContainers) {
118
- const containers = containers.filter(c => c.Image.includes("ge-dev"));
124
+ containers = containers.filter(c => c.Image.includes("ge-localdev"));
119
125
  }
120
126
 
121
127
  if (containers.length === 0) {
@@ -137,6 +143,18 @@ export async function down({ projectDir, allContainers = false }) {
137
143
  });
138
144
  })
139
145
  );
146
+
147
+ // remove each container
148
+ await Promise.all(
149
+ containers.map(c => {
150
+ const container = docker.getContainer(c.Id);
151
+ return container.remove({ force: true }).catch(err => {
152
+ console.error(`Failed to remove ${c.Names[0]}: ${err.message}`);
153
+ });
154
+ })
155
+ );
156
+
157
+ console.log('Containers stopped and removed.');
140
158
  }
141
159
 
142
160
  /**
@@ -214,17 +232,23 @@ export async function createDynamicNginxConf({ config, serviceConfDir, projectDi
214
232
  * @param {string} gnarHiddenDir
215
233
  * @param {string} projectDir
216
234
  * @param {boolean} coreDev - Whether to volume mount the core source code
235
+ * @param {boolean} bootstrapDev - Whether to set the cli/src/bootstrap directory as the project directory
236
+ * @param {boolean} build - Whether to re-build images
217
237
  * @param {boolean} test - Whether to run tests with ephemeral databases
218
238
  * @param {string} testService - The service to run tests for (only applicable if test=true)
219
239
  * @param {boolean} attachAll - Whether to attach to all containers' stdio (otherwise databases and message queue are detached)
220
240
  */
221
- async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, test = false, testService, attachAll = false }) {
241
+ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, bootstrapDev = false, build = false, test = false, testService, attachAll = false }) {
242
+
222
243
  let mysqlPortsCounter = 3306;
223
244
  let mongoPortsCounter = 27017;
224
245
  let mysqlHostsRequired = [];
225
246
  let mongoHostsRequired = [];
226
247
  const services = {};
227
248
 
249
+ console.log('======== g n a r e n g i n e ========');
250
+ console.log('⛏️ Starting development environment...');
251
+
228
252
  // test mode env var adjustments
229
253
  for (const svc of config.services) {
230
254
  if (test) {
@@ -242,63 +266,92 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
242
266
  }
243
267
  }
244
268
 
269
+ // create bridge network
270
+ const networkName = `ge-${config.environment}-${config.namespace}`;
271
+ createBridgeNetwork({
272
+ name: networkName
273
+ })
274
+
245
275
  // provision the provisioner service
246
- services['provisioner'] = {
247
- container_name: `ge-${config.environment}-${config.namespace}-provisioner`,
248
- image: `ge-${config.environment}-${config.namespace}-provisioner`,
249
- build: {
276
+ const provisionerTag = `ge-${config.environment}-${config.namespace}-provisioner`;
277
+
278
+ if (build) {
279
+ await buildImage({
250
280
  context: directories.provisioner,
251
- dockerfile: `./Dockerfile`
252
- },
253
- environment: {
254
- PROVISIONER_SECRETS: JSON.stringify(secrets)
255
- },
256
- volumes: [
257
- `${directories.provisioner}/src:/usr/gnar_engine/app/src`
258
- ],
259
- restart: 'no',
260
- attach: attachAll
281
+ dockerfile: 'Dockerfile',
282
+ imageTag: provisionerTag
283
+ });
261
284
  }
262
285
 
286
+ const __filename = fileURLToPath(import.meta.url);
287
+ const __dirname = path.dirname(__filename);
288
+
289
+ const provisionerBinds = [
290
+ `${path.resolve(__dirname, '../provisioner', 'src')}:/usr/gnar_engine/app/src`
291
+ ];
292
+
263
293
  if (coreDev) {
264
- services['provisioner'].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
294
+ provisionerBinds.push(`${gnarEngineCliConfig.coreDevPath}:${gnarEngineCliConfig.corePath}`);
265
295
  }
266
296
 
267
- // nginx
268
- services['nginx'] = {
297
+ const provisioner = await createContainer({
298
+ name: provisionerTag,
299
+ image: provisionerTag,
300
+ env: {
301
+ PROVISIONER_SECRETS: JSON.stringify(secrets)
302
+ },
303
+ ports: {},
304
+ binds: provisionerBinds,
305
+ restart: 'no',
306
+ attach: attachAll,
307
+ network: networkName
308
+ });
309
+ services[provisionerTag] = provisioner;
310
+
311
+ // Nginx
312
+ const nginxName = `ge-${config.environment}-${config.namespace}-nginx`;
313
+ services[nginxName] = await createContainer({
314
+ name: nginxName,
269
315
  image: 'nginx:latest',
270
- container_name: `ge-${config.environment}-${config.namespace}-nginx`,
271
- ports: [
272
- "80:80",
273
- "443:443"
274
- ],
275
- volumes: [
316
+ ports: { 80: 80, 443: 443 },
317
+ binds: [
276
318
  `${gnarHiddenDir}/nginx/nginx.conf:/etc/nginx/nginx.conf`,
277
319
  `${gnarHiddenDir}/nginx/service_conf:/etc/nginx/service_conf`
278
320
  ],
279
- restart: 'always',
280
- attach: attachAll
281
- }
321
+ attach: attachAll,
322
+ network: networkName
323
+ });
282
324
 
283
- // rabbit
284
- services['rabbitmq'] = {
325
+ // Rabbit MQ
326
+ const rabbitMqName = `ge-${config.environment}-${config.namespace}-rabbitmq`;
327
+ services[rabbitMqName] = await createContainer({
328
+ name: rabbitMqName,
285
329
  image: 'rabbitmq:management',
286
- container_name: `ge-${config.environment}-${config.namespace}-rabbitmq`,
287
- ports: [
288
- "5672:5672",
289
- "15672:15672"
290
- ],
291
- environment: {
330
+ env: {
292
331
  RABBITMQ_DEFAULT_USER: secrets.global.RABBITMQ_USER || '',
293
332
  RABBITMQ_DEFAULT_PASS: secrets.global.RABBITMQ_PASS || ''
294
333
  },
295
- restart: 'always',
296
- attach: attachAll
297
- }
334
+ ports: { 5672: 5672, 15672: 15672 },
335
+ binds: [],
336
+ attach: attachAll,
337
+ network: networkName,
338
+ aliases: ['rabbitmq']
339
+ });
298
340
 
299
341
  // services
300
342
  for (const svc of config.services) {
301
343
 
344
+ // build service image
345
+ const svcTag = `ge-${config.environment}-${config.namespace}-${svc.name}`;
346
+
347
+ if (build) {
348
+ await buildImage({
349
+ context: path.resolve(projectDir, 'services', svc.name),
350
+ dockerfile: 'Dockerfile',
351
+ imageTag: svcTag
352
+ });
353
+ }
354
+
302
355
  // env variables
303
356
  const serviceEnvVars = secrets.services?.[svc.name] || {};
304
357
  const localisedServiceEnvVars = {};
@@ -320,29 +373,35 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
320
373
  }
321
374
  }
322
375
 
323
- // service block
324
- services[`${svc.name}-service`] = {
325
- container_name: `ge-${config.environment}-${config.namespace}-${svc.name}`,
326
- image: `ge-${config.environment}-${config.namespace}-${svc.name}`,
327
- build: {
328
- context: projectDir,
329
- dockerfile: `./services/${svc.name}/Dockerfile`
330
- },
331
- command: svc.command || [],
332
- environment: env,
333
- ports: svc.ports || [],
334
- depends_on: svc.depends_on || [],
335
- volumes: [
336
- `${projectDir}/services/${svc.name}/src:/usr/gnar_engine/app/src`
337
- ],
338
- restart: 'always'
339
- };
340
-
341
376
  // add the core source code mount if in coreDev mode
377
+ const serviceVolumes = [
378
+ `${path.resolve(projectDir, 'services', svc.name, 'src')}:/usr/gnar_engine/app/src`
379
+ ];
380
+
342
381
  if (coreDev) {
343
- services[`${svc.name}-service`].volumes.push(`../../../core/:${gnarEngineCliConfig.corePath}`);
382
+ serviceVolumes.push(`${gnarEngineCliConfig.coreDevPath}:${gnarEngineCliConfig.corePath}`);
344
383
  }
345
384
 
385
+ // split from "port:port" to { port: port }
386
+ const ports = {};
387
+ for (const portMapping of svc.ports || []) {
388
+ const [hostPort, containerPort] = portMapping.split(':').map(p => parseInt(p, 10));
389
+ ports[containerPort] = hostPort;
390
+ }
391
+
392
+ services[svcTag] = await createContainer({
393
+ name: svcTag,
394
+ image: svcTag,
395
+ command: svc.command || [],
396
+ env: env,
397
+ ports: ports,
398
+ binds: serviceVolumes,
399
+ restart: 'always',
400
+ attach: true,
401
+ network: networkName,
402
+ aliases: [`${svc.name}-service`]
403
+ });
404
+
346
405
  // check if mysql service required
347
406
  if (
348
407
  serviceEnvVars.MYSQL_HOST &&
@@ -367,29 +426,30 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
367
426
  continue;
368
427
  }
369
428
 
370
- services[host] = {
371
- container_name: `ge-${config.environment}-${config.namespace}-${host}`,
429
+ const mysqlContainerName = `ge-${config.environment}-${config.namespace}-${host}`;
430
+ services[mysqlContainerName] = await createContainer({
431
+ name: `ge-${config.environment}-${config.namespace}-${host}`,
372
432
  image: 'mysql',
373
- ports: [
374
- `${mysqlPortsCounter}:${mysqlPortsCounter}`
375
- ],
376
- restart: 'always',
377
- environment: {
433
+ env: {
378
434
  MYSQL_HOST: host,
379
435
  MYSQL_ROOT_PASSWORD: secrets.provision.MYSQL_ROOT_PASSWORD
380
436
  },
381
- volumes: [
437
+ ports: {
438
+ [mysqlPortsCounter]: mysqlPortsCounter
439
+ },
440
+ binds: [
382
441
  `${gnarHiddenDir}/data/${host}-data:/var/lib/mysql`
383
442
  ],
384
- attach: attachAll
385
- };
443
+ restart: 'always',
444
+ attach: attachAll,
445
+ network: networkName,
446
+ aliases: [host]
447
+ });
386
448
 
387
449
  mysqlPortsCounter++;
388
450
  }
389
-
390
- services['provisioner'].depends_on = [...new Set(mysqlHostsRequired)];
391
451
  }
392
-
452
+
393
453
  // add mongo hosts if required
394
454
  if (mongoHostsRequired.length > 0) {
395
455
  for (const host of mongoHostsRequired) {
@@ -397,33 +457,37 @@ async function createDynamicDockerCompose({ config, secrets, gnarHiddenDir, proj
397
457
  continue;
398
458
  }
399
459
 
400
- services[host] = {
401
- container_name: `ge-${config.environment}-${config.namespace}-${host}`,
460
+ const mongoContainerName = `ge-${config.environment}-${config.namespace}-${host}`;
461
+ services[mongoContainerName] = await createContainer({
462
+ name: `ge-${config.environment}-${config.namespace}-${host}`,
402
463
  image: 'mongo:latest',
403
- ports: [
404
- `${mongoPortsCounter}:27017`
405
- ],
406
- restart: 'always',
407
- environment: {
464
+ env: {
408
465
  MONGO_INITDB_ROOT_USERNAME: 'root',
409
466
  MONGO_INITDB_ROOT_PASSWORD: secrets.provision.MONGO_ROOT_PASSWORD
410
467
  },
411
- volumes: [
468
+ ports: {
469
+ [mongoPortsCounter]: 27017
470
+ },
471
+ binds: [
412
472
  `${gnarHiddenDir}/data/${host}-data:/data/db`,
413
- './mongo-init-scripts:/docker-entrypoint-initdb.d'
473
+ //'./mongo-init-scripts:/docker-entrypoint-initdb.d'
414
474
  ],
415
- attach: attachAll
416
- };
475
+ restart: 'always',
476
+ attach: attachAll,
477
+ network: networkName,
478
+ aliases: [host]
479
+ });
417
480
 
418
481
  // increment mongo port for next service as required
419
482
  mongoPortsCounter++;
420
483
  }
421
484
  }
422
485
 
423
- return {
424
- version: "3.9",
425
- services
426
- }
486
+ // start the containers
487
+ Object.keys(services).forEach(async (key) => {
488
+ const container = services[key];
489
+ container.start();
490
+ });
427
491
  }
428
492
 
429
493
  /**
@@ -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)) {
@@ -1,6 +1,7 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { profiles } from '../profiles/profiles.client.js';
3
3
  import { scaffolder } from './scaffolder.handler.js';
4
+ import { helpers } from '../helpers/helpers.js';
4
5
  import path from 'path';
5
6
 
6
7
  export const registerScaffolderCommands = (program) => {
@@ -33,11 +34,16 @@ export const registerScaffolderCommands = (program) => {
33
34
  }
34
35
  ]);
35
36
 
37
+ // validate absolute path, if it is not absolute, make it absolute
38
+ if (!path.isAbsolute(answers.projectDir)) {
39
+ answers.projectDir = path.join(process.cwd(), answers.projectDir);
40
+ }
41
+
36
42
  // create the project
37
43
  try {
38
44
  scaffolder.createNewProject({
39
45
  projectName: projectName,
40
- projectDir: answers.projectDir,
46
+ projectDir: path.join('/', answers.projectDir),
41
47
  rootAdminEmail: answers.rootAdminEmail
42
48
  });
43
49
  } catch (error) {
@@ -127,4 +133,54 @@ export const registerScaffolderCommands = (program) => {
127
133
  }
128
134
  }
129
135
  });
136
+
137
+ create
138
+ .command('entity <entity>')
139
+ .description('📦 Create a new entity in an existing service')
140
+ .option('--in-service <serviceName>', 'The service in which to add the entity')
141
+ .action(async (entity, options) => {
142
+ // validate
143
+ if (!entity) {
144
+ console.error('❌ Please specify an entity name using gnar create entity <entityName> --in-service <serviceName>');
145
+ }
146
+ if (!options.inService) {
147
+ console.error('❌ Please specify the service using --in-service <serviceName>');
148
+ }
149
+
150
+ let activeProfile;
151
+ try {
152
+ activeProfile = profiles.getActiveProfile();
153
+ } catch (error) {
154
+ console.error('❌ No active profile found. Please create or set one using `gnar profile create` or `gnar profile set-active <profileName>`');
155
+ return;
156
+ }
157
+
158
+ // create the entity
159
+ try {
160
+ // add trailing slash to project dir if missing
161
+ let projectDir = activeProfile.profile.PROJECT_DIR;
162
+
163
+ if (!activeProfile.profile.PROJECT_DIR.endsWith(path.sep)) {
164
+ projectDir += path.sep;
165
+ }
166
+
167
+ const dbType = await helpers.getDbTypeFromSecrets(options.inService, projectDir);
168
+ const serviceDir = path.join(projectDir, 'services', options.inService.toLowerCase());
169
+
170
+ console.log('Creating new entity in... ' + serviceDir);
171
+
172
+ scaffolder.createNewEntity({
173
+ entityName: entity,
174
+ inService: options.inService,
175
+ serviceDir: serviceDir,
176
+ database: dbType
177
+ });
178
+
179
+ console.log('Created entity ' + entity + ' in service ' + options.inService);
180
+ console.log('👉 Remember to add the new entities handler and controllers to your service\'s app.js');
181
+
182
+ } catch (error) {
183
+ console.error('❌ Error creating entity:', error.message);
184
+ }
185
+ });
130
186
  }