@memberjunction/cli 5.4.0 → 5.5.0

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 (49) hide show
  1. package/dist/commands/doctor/index.d.ts +12 -0
  2. package/dist/commands/doctor/index.d.ts.map +1 -0
  3. package/dist/commands/doctor/index.js +69 -0
  4. package/dist/commands/doctor/index.js.map +1 -0
  5. package/dist/commands/install/index.d.ts +18 -136
  6. package/dist/commands/install/index.d.ts.map +1 -1
  7. package/dist/commands/install/index.js +189 -390
  8. package/dist/commands/install/index.js.map +1 -1
  9. package/dist/commands/sql-audit/index.d.ts +25 -0
  10. package/dist/commands/sql-audit/index.d.ts.map +1 -0
  11. package/dist/commands/sql-audit/index.js +198 -0
  12. package/dist/commands/sql-audit/index.js.map +1 -0
  13. package/dist/commands/sql-convert/index.d.ts +30 -0
  14. package/dist/commands/sql-convert/index.d.ts.map +1 -0
  15. package/dist/commands/sql-convert/index.js +128 -0
  16. package/dist/commands/sql-convert/index.js.map +1 -0
  17. package/dist/commands/translate-sql/index.d.ts +39 -0
  18. package/dist/commands/translate-sql/index.d.ts.map +1 -0
  19. package/dist/commands/translate-sql/index.js +229 -0
  20. package/dist/commands/translate-sql/index.js.map +1 -0
  21. package/dist/lib/legacy-install.d.ts +29 -0
  22. package/dist/lib/legacy-install.d.ts.map +1 -0
  23. package/dist/lib/legacy-install.js +391 -0
  24. package/dist/lib/legacy-install.js.map +1 -0
  25. package/dist/light-commands.d.ts.map +1 -1
  26. package/dist/light-commands.js +6 -1
  27. package/dist/light-commands.js.map +1 -1
  28. package/dist/translate-sql/classifier.d.ts +27 -0
  29. package/dist/translate-sql/classifier.d.ts.map +1 -0
  30. package/dist/translate-sql/classifier.js +118 -0
  31. package/dist/translate-sql/classifier.js.map +1 -0
  32. package/dist/translate-sql/groundTruth.d.ts +25 -0
  33. package/dist/translate-sql/groundTruth.d.ts.map +1 -0
  34. package/dist/translate-sql/groundTruth.js +93 -0
  35. package/dist/translate-sql/groundTruth.js.map +1 -0
  36. package/dist/translate-sql/index.d.ts +5 -0
  37. package/dist/translate-sql/index.d.ts.map +1 -0
  38. package/dist/translate-sql/index.js +5 -0
  39. package/dist/translate-sql/index.js.map +1 -0
  40. package/dist/translate-sql/reportGenerator.d.ts +26 -0
  41. package/dist/translate-sql/reportGenerator.d.ts.map +1 -0
  42. package/dist/translate-sql/reportGenerator.js +83 -0
  43. package/dist/translate-sql/reportGenerator.js.map +1 -0
  44. package/dist/translate-sql/ruleTranslator.d.ts +21 -0
  45. package/dist/translate-sql/ruleTranslator.d.ts.map +1 -0
  46. package/dist/translate-sql/ruleTranslator.js +74 -0
  47. package/dist/translate-sql/ruleTranslator.js.map +1 -0
  48. package/oclif.manifest.json +522 -158
  49. package/package.json +14 -12
@@ -1,418 +1,217 @@
1
- import { confirm, input, select } from '@inquirer/prompts';
2
- import dotenv from 'dotenv';
3
- import recast from 'recast';
1
+ import { select, confirm, input } from '@inquirer/prompts';
4
2
  import { Command, Flags } from '@oclif/core';
5
- import fs from 'fs-extra';
6
- import { execSync } from 'node:child_process';
7
- import os from 'node:os';
8
- import path from 'node:path';
9
- import { ZodError, z } from 'zod';
10
- // Directories are relative to execution cwd
11
- const GENERATED_ENTITIES_DIR = 'GeneratedEntities';
12
- const SQL_SCRIPTS_DIR = 'SQL Scripts';
13
- const GENERATED_DIR = 'generated';
14
- const MJ_BASE_DIR = 'MJ_BASE';
15
- const MJAPI_DIR = 'MJAPI';
16
- const MJEXPLORER_DIR = 'MJExplorer';
17
- const configSchema = z.object({
18
- dbUrl: z.string().min(1),
19
- dbInstance: z.string(),
20
- dbTrustServerCertificate: z.coerce
21
- .boolean()
22
- .default(false)
23
- .transform((v) => (v ? 'Y' : 'N')),
24
- dbDatabase: z.string().min(1),
25
- dbPort: z.number({ coerce: true }).int().positive(),
26
- codeGenLogin: z.string(),
27
- codeGenPwD: z.string(),
28
- mjAPILogin: z.string(),
29
- mjAPIPwD: z.string(),
30
- graphQLPort: z.number({ coerce: true }).int().positive().optional(),
31
- authType: z.enum(['MSAL', 'AUTH0', 'BOTH']),
32
- msalWebClientId: z.string().optional(),
33
- msalTenantId: z.string().optional(),
34
- auth0ClientId: z.string().optional(),
35
- auth0ClientSecret: z.string().optional(),
36
- auth0Domain: z.string().optional(),
37
- createNewUser: z.coerce.boolean().optional(),
38
- userEmail: z.string().email().or(z.literal('')).optional().default(''),
39
- userFirstName: z.string().optional(),
40
- userLastName: z.string().optional(),
41
- userName: z.string().optional(),
42
- openAIAPIKey: z.string().optional(),
43
- anthropicAPIKey: z.string().optional(),
44
- mistralAPIKey: z.string().optional(),
45
- });
3
+ import chalk from 'chalk';
4
+ import { InstallerEngine, } from '@memberjunction/installer';
5
+ import { LegacyInstaller } from '../../lib/legacy-install.js';
46
6
  export default class Install extends Command {
47
- static { this.description = 'Install MemberJunction'; }
7
+ static { this.description = 'Install MemberJunction from a GitHub release'; }
48
8
  static { this.examples = [
49
- `<%= config.bin %> <%= command.id %>
50
- `,
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> -t v4.3.0',
11
+ '<%= config.bin %> <%= command.id %> --dry-run',
12
+ '<%= config.bin %> <%= command.id %> --yes',
51
13
  ]; }
52
14
  static { this.flags = {
53
- verbose: Flags.boolean({ char: 'v', description: 'Enable additional logging' }),
15
+ tag: Flags.string({
16
+ char: 't',
17
+ description: 'Release tag to install (e.g. v4.3.0). If omitted, shows a version picker.',
18
+ }),
19
+ dir: Flags.string({
20
+ description: 'Target directory for the installation',
21
+ default: '.',
22
+ }),
23
+ config: Flags.string({
24
+ char: 'c',
25
+ description: 'Path to JSON config file with install settings (database, auth, ports, etc.)',
26
+ }),
27
+ legacy: Flags.boolean({
28
+ description: 'Use the legacy interactive installer (ZIP-only distribution)',
29
+ hidden: true,
30
+ }),
31
+ 'dry-run': Flags.boolean({
32
+ description: 'Show the install plan without executing it',
33
+ }),
34
+ yes: Flags.boolean({
35
+ char: 'y',
36
+ description: 'Non-interactive mode: auto-select latest version and accept defaults',
37
+ }),
38
+ verbose: Flags.boolean({
39
+ char: 'v',
40
+ description: 'Show detailed output',
41
+ }),
42
+ 'skip-db': Flags.boolean({
43
+ description: 'Skip database provisioning phases',
44
+ }),
45
+ 'skip-start': Flags.boolean({
46
+ description: 'Skip service startup and smoke tests',
47
+ }),
48
+ 'no-resume': Flags.boolean({
49
+ description: 'Ignore any existing checkpoint and start fresh',
50
+ }),
51
+ fast: Flags.boolean({
52
+ description: 'Fast mode: skip smoke test and optimize post-codegen steps. Re-run without --fast if you encounter issues.',
53
+ }),
54
54
  }; }
55
55
  async run() {
56
- const parsed = await this.parse(Install);
57
- this.flags = parsed.flags;
58
- this.checkNodeVersion();
59
- this.checkAvailableDiskSpace(2);
60
- this.verifyDirs(GENERATED_ENTITIES_DIR, SQL_SCRIPTS_DIR, MJAPI_DIR, MJEXPLORER_DIR);
61
- this.userConfig = await this.getUserConfiguration();
62
- this.log('Setting up MemberJunction Distribution...');
63
- if (this.flags.verbose) {
64
- this.logJson({ userConfig: this.userConfig, flags: this.flags });
56
+ const { flags } = await this.parse(Install);
57
+ if (flags.legacy) {
58
+ const legacy = new LegacyInstaller(this, flags.verbose ?? false);
59
+ return legacy.Run();
65
60
  }
66
- //*******************************************************************
67
- // Process GeneratedEntities
68
- //*******************************************************************
69
- this.log('\nBootstrapping GeneratedEntities...');
70
- this.log('Running npm install...');
71
- execSync('npm install', { stdio: 'inherit', cwd: GENERATED_ENTITIES_DIR });
72
- //*******************************************************************
73
- // Process Config
74
- //*******************************************************************
75
- this.log('\nProcessing Config...');
76
- this.log(' Updating ');
77
- this.log(' Setting up .env and mj.config.cjs...');
78
- const dotenvContent = `#Database Setup
79
- DB_HOST='${this.userConfig.dbUrl}'
80
- DB_PORT=${this.userConfig.dbPort}
81
- CODEGEN_DB_USERNAME='${this.userConfig.codeGenLogin}'
82
- CODEGEN_DB_PASSWORD='${this.userConfig.codeGenPwD}'
83
- DB_USERNAME='${this.userConfig.mjAPILogin}'
84
- DB_PASSWORD='${this.userConfig.mjAPIPwD}'
85
- DB_DATABASE='${this.userConfig.dbDatabase}'
86
- ${this.userConfig.dbInstance ? "DB_INSTANCE_NAME='" + this.userConfig.dbInstance + "'" : ''}
87
- ${this.userConfig.dbTrustServerCertificate === 'Y' ? 'DB_TRUST_SERVER_CERTIFICATE=1' : ''}
88
-
89
- #OUTPUT CODE is used for output directories like SQL Scripts
90
- OUTPUT_CODE='${this.userConfig.dbDatabase}'
91
-
92
- # Name of the schema that MJ has been setup in. This defaults to __mj
93
- MJ_CORE_SCHEMA='__mj'
94
-
95
- # If using Advanced Generation or the MJAI library, populate this with the API key for the AI vendor you are using
96
- # Also, you need to configure the settings under advancedGeneration in the mj.config.cjs file, including choosing the vendor.
97
- AI_VENDOR_API_KEY__OpenAILLM='${this.userConfig.openAIAPIKey}'
98
- AI_VENDOR_API_KEY__MistralLLM='${this.userConfig.mistralAPIKey}'
99
- AI_VENDOR_API_KEY__AnthropicLLM='${this.userConfig.anthropicAPIKey}'
100
-
101
- PORT=${this.userConfig.graphQLPort}
102
-
103
- UPDATE_USER_CACHE_WHEN_NOT_FOUND=1
104
- UPDATE_USER_CACHE_WHEN_NOT_FOUND_DELAY=5000
105
-
106
- # AUTHENTICATION SECTION - you can use MSAL or Auth0 or both for authentication services for MJAPI
107
- # MSAL Section
108
- WEB_CLIENT_ID=${this.userConfig.msalWebClientId}
109
- TENANT_ID=${this.userConfig.msalTenantId}
110
-
111
- # Auth0 Section
112
- AUTH0_CLIENT_ID=${this.userConfig.auth0ClientId}
113
- AUTH0_CLIENT_SECRET=${this.userConfig.auth0ClientSecret}
114
- AUTH0_DOMAIN=${this.userConfig.auth0Domain}
115
-
116
- # Skip API URL, KEY and Org ID
117
- # YOU MUST ENTER IN THE CORRECT URL and ORG ID for your Skip API USE BELOW
118
- ASK_SKIP_API_URL = 'http://localhost:8000'
119
- ASK_SKIP_ORGANIZATION_ID = 1
120
- `;
121
- fs.writeFileSync('.env', dotenvContent);
122
- //*******************************************************************
123
- // Process MJAPI
124
- //*******************************************************************
125
- this.log('\n\nBootstrapping MJAPI...');
126
- this.log(' Running npm link for generated code...');
127
- execSync('npm link ../GeneratedEntities ../GeneratedActions', { stdio: 'inherit', cwd: MJAPI_DIR });
128
- this.log('Running CodeGen...');
129
- this.renameFolderToMJ_BASE(this.userConfig.dbDatabase);
130
- // next, run CodeGen
131
- // We do not manually run the compilation for GeneratedEntities because CodeGen handles that, but notice above that we did npm install for GeneratedEntities otherwise when CodeGen attempts to compile it, it will fail.
132
- dotenv.config({ quiet: true });
133
- this.config.runCommand('codegen');
134
- // Process MJExplorer
135
- this.log('\nProcessing MJExplorer...');
136
- this.log('\n Updating environment files...');
137
- const config = {
138
- CLIENT_ID: this.userConfig.msalWebClientId,
139
- TENANT_ID: this.userConfig.msalTenantId,
140
- CLIENT_AUTHORITY: this.userConfig.msalTenantId ? `https://login.microsoftonline.com/${this.userConfig.msalTenantId}` : '',
141
- AUTH_TYPE: this.userConfig.authType === 'AUTH0' ? 'auth0' : this.userConfig.authType.toLowerCase(),
142
- AUTH0_DOMAIN: this.userConfig.auth0Domain,
143
- AUTH0_CLIENTID: this.userConfig.auth0ClientId,
144
- };
145
- await this.updateEnvironmentFiles(path.join(MJEXPLORER_DIR, 'src', 'environments'), config);
146
- // keep on going with MJ Explorer - do the rest of the stuff
147
- this.log(' Running npm link for GeneratedEntities...');
148
- execSync('npm link ../GeneratedEntities', { stdio: 'inherit', cwd: MJEXPLORER_DIR });
149
- this.log('Installation complete!');
61
+ return this.runEngineInstall(flags);
150
62
  }
151
- async getUserConfiguration() {
152
- let userConfig;
153
- try {
154
- const configObject = await fs.readJSON('install.config.json');
155
- userConfig = configSchema.parse(configObject);
156
- }
157
- catch (e) {
158
- if (e instanceof ZodError) {
159
- this.log(`Invalid config file found at '${path.join(fs.realpathSync('.'), 'install.config.json')}'${this.flags.verbose ? '' : ', retry with --verbose for details'}`);
160
- if (this.flags.verbose) {
161
- console.table(e.issues);
162
- }
163
- }
164
- else {
165
- this.log(`No config file found at '${path.join(fs.realpathSync('.'), 'install.config.json')}'`);
166
- }
167
- }
168
- if (!userConfig) {
169
- this.log('\n>>> Please answer the following questions to setup the .env files for CodeGen. After this process you can manually edit the .env file as desired.');
170
- const dbUrl = await input({
171
- message: 'Enter the database server hostname:',
172
- validate: (v) => configSchema.shape.dbDatabase.safeParse(v).success,
173
- });
174
- const dbInstance = await input({
175
- message: 'If you are using a named instance on that server, if so, enter the name here, if not leave blank:',
176
- });
177
- const dbTrustServerCertificate = (await confirm({
178
- message: 'Does the database server use a self-signed certificate? If you are using a local instance, enter Y:',
179
- }))
180
- ? 'Y'
181
- : 'N';
182
- const dbDatabase = await input({
183
- message: 'Enter the database name on that server:',
184
- validate: (v) => configSchema.shape.dbDatabase.safeParse(v).success,
185
- });
186
- const dbPort = await input({
187
- message: 'Enter the port the database server listens on',
188
- validate: (v) => configSchema.shape.dbPort.safeParse(v).success,
189
- default: '1433',
190
- });
191
- const codeGenLogin = await input({ message: 'Enter the database login for CodeGen:' });
192
- const codeGenPwD = await input({ message: 'Enter the database password for CodeGen:' });
193
- this.log('\n>>> Please answer the following questions to setup the .env files for MJAPI. After this process you can manually edit the .env file in CodeGen as desired.');
194
- const mjAPILogin = await input({ message: 'Enter the database login for MJAPI:' });
195
- const mjAPIPwD = await input({ message: 'Enter the database password for MJAPI:' });
196
- const graphQLPort = await input({
197
- message: 'Enter the port to use for the GraphQL API',
198
- validate: (v) => configSchema.shape.graphQLPort.safeParse(v).success,
199
- default: '4000',
200
- });
201
- const authType = await select({
202
- message: 'Will you be using Microsoft Entra (formerly Azure AD), Auth0, or both for authentication services for MJAPI:',
203
- choices: [
204
- { name: 'Microsoft Entra (MSAL)', value: 'MSAL' },
205
- { name: 'Auth0', value: 'AUTH0' },
206
- { name: 'Both', value: 'BOTH' },
207
- ],
208
- });
209
- const msalTenantId = ['BOTH', 'MSAL'].includes(authType) ? await input({ message: 'Enter the web client ID for Entra:' }) : '';
210
- const msalWebClientId = ['BOTH', 'MSAL'].includes(authType) ? await input({ message: 'Enter the tenant ID for Entra:' }) : '';
211
- const auth0ClientId = ['BOTH', 'AUTH0'].includes(authType) ? await input({ message: 'Enter the client ID for Auth0:' }) : '';
212
- const auth0ClientSecret = ['BOTH', 'AUTH0'].includes(authType) ? await input({ message: 'Enter the client secret for Auth0:' }) : '';
213
- const auth0Domain = ['BOTH', 'AUTH0'].includes(authType) ? await input({ message: 'Enter the domain for Auth0:' }) : '';
214
- const createNewUser = await confirm({ message: 'Do you want to create a new user in the database? (Y/N):' });
215
- const userEmail = createNewUser
216
- ? await input({
217
- message: 'Enter the new user email',
218
- validate: (v) => configSchema.shape.userEmail.safeParse(v).success,
219
- })
220
- : '';
221
- const userFirstName = createNewUser ? await input({ message: 'Enter the new user first name:' }) : '';
222
- const userLastName = createNewUser ? await input({ message: 'Enter the new user last name::' }) : '';
223
- const userName = createNewUser
224
- ? await input({ message: 'Enter the new user name (leave blank to use email):', default: userEmail })
225
- : '';
226
- const openAIAPIKey = await input({ message: 'Enter the OpenAI API Key (leave blank if not using):' });
227
- const anthropicAPIKey = await input({ message: 'Enter the Anthropic API Key (leave blank if not using):' });
228
- const mistralAPIKey = await input({ message: 'Enter the Mistral API Key (leave blank if not using):' });
229
- userConfig = configSchema.parse({
230
- dbUrl,
231
- dbInstance,
232
- dbTrustServerCertificate,
233
- dbDatabase,
234
- dbPort,
235
- codeGenLogin,
236
- codeGenPwD,
237
- mjAPILogin,
238
- mjAPIPwD,
239
- graphQLPort,
240
- authType,
241
- msalWebClientId,
242
- msalTenantId,
243
- auth0ClientId,
244
- auth0ClientSecret,
245
- auth0Domain,
246
- createNewUser: createNewUser ? 'Y' : 'N',
247
- userEmail,
248
- userFirstName,
249
- userLastName,
250
- userName,
251
- openAIAPIKey,
252
- anthropicAPIKey,
253
- mistralAPIKey,
254
- });
255
- }
256
- return userConfig;
257
- }
258
- /**
259
- * Verifies that the specified directories exist.
260
- * @param {...string} dirs - The directories to check.
261
- */
262
- verifyDirs(...dirs) {
263
- dirs.forEach((dir) => {
264
- if (!fs.existsSync(dir)) {
265
- this.error(`Unable to locate required package at '${path.join(fs.realpathSync('.'), dir)}'`, {
266
- exit: 1,
267
- suggestions: ['Run the install from the same directory as the extracted MemberJunction distribution'],
268
- });
269
- }
63
+ // ---------------------------------------------------------------------------
64
+ // Engine-based install
65
+ // ---------------------------------------------------------------------------
66
+ async runEngineInstall(flags) {
67
+ const engine = new InstallerEngine();
68
+ const fast = flags.fast ?? false;
69
+ this.wireEventHandlers(engine, flags.verbose ?? false);
70
+ const plan = await engine.CreatePlan({
71
+ Tag: flags.tag,
72
+ Dir: flags.dir,
73
+ SkipDB: flags['skip-db'],
74
+ SkipStart: flags['skip-start'],
75
+ Fast: fast,
270
76
  });
271
- }
272
- checkNodeVersion() {
273
- const validNodeVersion = Number(process.version.replace(/^v(\d+).*/, '$1')) >= 20;
274
- if (!validNodeVersion) {
275
- this.error('MemberJunction requires Node.js version 20 or higher.', { exit: 1 });
77
+ if (flags['dry-run']) {
78
+ this.renderDryRun(plan);
79
+ return;
276
80
  }
81
+ this.renderHeader();
82
+ const result = await engine.Run(plan, {
83
+ Yes: flags.yes,
84
+ Verbose: flags.verbose,
85
+ NoResume: flags['no-resume'],
86
+ ConfigFile: flags.config,
87
+ Fast: fast,
88
+ });
89
+ this.renderResult(result, fast);
277
90
  }
278
- /**
279
- * Checks if there is at least `numGB` GB of free disk space.
280
- * @param {number} numGB - The number of GB to check for.
281
- * @returns {boolean} True if there is enough free disk space, false otherwise.
282
- */
283
- checkAvailableDiskSpace(numGB = 2) {
284
- try {
285
- this.log(`Checking for at least ${numGB}GB of free disk space...`);
286
- // Define numGB GB in bytes
287
- const GBToBytes = 1024 * 1024 * 1024;
288
- const requiredSpace = numGB * GBToBytes;
289
- let freeSpace;
290
- if (os.platform() === 'win32') {
291
- // For Windows, check the C: drive
292
- const command = `wmic LogicalDisk where DeviceID="C:" get FreeSpace`;
293
- const output = execSync(command).toString();
294
- const lines = output.trim().split('\n');
295
- freeSpace = parseInt(lines[1].trim());
91
+ // ---------------------------------------------------------------------------
92
+ // Event wiring bridges engine events to CLI output + inquirer prompts
93
+ // ---------------------------------------------------------------------------
94
+ wireEventHandlers(engine, verbose) {
95
+ engine.On('prompt', (event) => {
96
+ this.handlePromptEvent(event);
97
+ });
98
+ engine.On('phase:start', (event) => {
99
+ this.log(chalk.cyan(`\u25b8 ${event.Description}`));
100
+ });
101
+ engine.On('phase:end', (event) => {
102
+ const duration = chalk.dim(`(${this.formatDuration(event.DurationMs)})`);
103
+ if (event.Status === 'completed') {
104
+ this.log(chalk.green(` \u2713 ${event.Phase} completed ${duration}`));
296
105
  }
297
- else {
298
- // For POSIX systems, check the root directory
299
- const command = `df -k / | tail -1 | awk '{ print $4; }'`;
300
- freeSpace = parseInt(execSync(command).toString().trim()) * 1024;
106
+ else if (event.Status === 'failed') {
107
+ this.log(chalk.red(` \u2717 ${event.Phase} failed ${duration}`));
301
108
  }
302
- if (freeSpace >= requiredSpace) {
303
- this.log(` Sufficient disk space available: ${Math.round(freeSpace / GBToBytes)} GB`);
304
- return true;
109
+ });
110
+ engine.On('step:progress', (event) => {
111
+ if (verbose) {
112
+ const percent = event.Percent != null ? ` (${event.Percent}%)` : '';
113
+ this.log(chalk.dim(` ${event.Message}${percent}`));
305
114
  }
306
- else {
307
- this.error(`Insufficient disk space. Required: ${requiredSpace} bytes, Available: ${Math.round(freeSpace / GBToBytes)} GB`, {
308
- exit: 1,
309
- });
115
+ });
116
+ engine.On('log', (event) => {
117
+ if (event.Level === 'verbose' && !verbose)
118
+ return;
119
+ this.log(event.Message);
120
+ });
121
+ engine.On('warn', (event) => {
122
+ this.log(chalk.yellow(` \u26a0 ${event.Message}`));
123
+ });
124
+ engine.On('error', (event) => {
125
+ this.log(chalk.red(` \u2717 [${event.Phase}] ${event.Error.message}`));
126
+ if (event.Error.SuggestedFix) {
127
+ this.log(chalk.yellow(` \u2192 ${event.Error.SuggestedFix}`));
310
128
  }
311
- }
312
- catch (error) {
313
- this.logToStderr(this.toErrorJson(error));
314
- this.error('Error checking disk space', { exit: 1 });
315
- }
129
+ });
316
130
  }
317
- /**
318
- * Updates environment files in a given directory.
319
- * @param {string} dirPath - The path to the directory containing environment files.
320
- * @param {object} config - The configuration object with values to update.
321
- */
322
- async updateEnvironmentFiles(dirPath, config) {
323
- try {
324
- // Define the pattern for environment files.
325
- const envFilePattern = /environment.*\.ts$/;
326
- // Read all files in the directory.
327
- const files = await fs.readdir(dirPath);
328
- // Filter for environment files.
329
- const envFiles = files.filter((file) => envFilePattern.test(file));
330
- // Update each environment file.
331
- for (const file of envFiles) {
332
- if (this.flags.verbose) {
333
- this.log(`Updating ${file}`);
334
- }
335
- const filePath = path.join(dirPath, file);
336
- const data = await fs.readFile(filePath, 'utf8');
337
- // Replace the values in the file.
338
- let updatedData = data;
339
- Object.entries(config).forEach(([key, value = '']) => {
340
- const regex = new RegExp(`(["\']?${key}["\']?:\\s*["\'])([^"\']*)(["\'])`, 'g');
341
- const escapedValue = value.replaceAll('$', () => '$$');
342
- updatedData = updatedData.replace(regex, `$1${escapedValue}$3`);
131
+ async handlePromptEvent(event) {
132
+ let answer;
133
+ switch (event.PromptType) {
134
+ case 'select':
135
+ answer = await select({
136
+ message: event.Message,
137
+ choices: (event.Choices ?? []).map((c) => ({
138
+ name: c.Label,
139
+ value: c.Value,
140
+ })),
141
+ });
142
+ break;
143
+ case 'confirm': {
144
+ const confirmed = await confirm({
145
+ message: event.Message,
146
+ default: event.Default === 'yes' || event.Default === 'true',
343
147
  });
344
- // Write the updated data back to the file.
345
- await fs.writeFile(filePath, updatedData, 'utf8');
346
- this.log(`Updated ${file}`);
148
+ answer = confirmed ? 'yes' : 'no';
149
+ break;
347
150
  }
151
+ case 'input':
152
+ answer = await input({
153
+ message: event.Message,
154
+ default: event.Default,
155
+ });
156
+ break;
157
+ default:
158
+ answer = event.Default ?? '';
159
+ break;
348
160
  }
349
- catch (err) {
350
- console.error('Error:', err);
351
- }
161
+ event.Resolve(answer);
352
162
  }
353
- renameFolderToMJ_BASE(dbDatabase) {
354
- // rename the MJ_BASE set of SQL Scripts to our new dbname
355
- const oldFolderPath = path.join(SQL_SCRIPTS_DIR, GENERATED_DIR, MJ_BASE_DIR);
356
- const newFolderPath = path.join(SQL_SCRIPTS_DIR, GENERATED_DIR, dbDatabase); // Assuming dbDatabase holds the new name
357
- if (!fs.existsSync(oldFolderPath)) {
358
- this.warn(`SQL scripts not found at '${oldFolderPath}', skipping rename`);
163
+ // ---------------------------------------------------------------------------
164
+ // Rendering helpers
165
+ // ---------------------------------------------------------------------------
166
+ renderHeader() {
167
+ this.log('');
168
+ this.log(chalk.bold('MemberJunction Installer'));
169
+ this.log('\u2500'.repeat(23));
170
+ this.log('');
171
+ }
172
+ renderDryRun(plan) {
173
+ this.log('');
174
+ this.log(chalk.bold(plan.Summarize()));
175
+ this.log('');
176
+ this.log(chalk.dim('Run without --dry-run to execute this plan.'));
177
+ }
178
+ renderResult(result, fast = false) {
179
+ this.log('');
180
+ if (result.Success) {
181
+ this.log(chalk.green.bold('Installation completed successfully!'));
182
+ if (result.Warnings.length > 0) {
183
+ this.log(chalk.yellow(` ${result.Warnings.length} warning(s) during install.`));
184
+ }
185
+ this.log(chalk.dim(` Duration: ${this.formatDuration(result.DurationMs)}`));
186
+ this.log(chalk.dim(` Phases: ${result.PhasesCompleted.join(', ')}`));
187
+ if (fast) {
188
+ this.log('');
189
+ this.log(chalk.cyan(' Fast mode was used. Smoke test was skipped and some post-codegen'));
190
+ this.log(chalk.cyan(' rebuild steps were optimized. If you encounter runtime errors,'));
191
+ this.log(chalk.cyan(' re-run without --fast for a full install.'));
192
+ }
359
193
  return;
360
194
  }
361
- try {
362
- fs.moveSync(oldFolderPath, newFolderPath);
363
- this.log(`Renamed ${oldFolderPath} to ${newFolderPath} successfully.`);
364
- }
365
- catch (err) {
366
- this.logToStderr(`An error occurred while renaming the '${oldFolderPath}' folder:`, err);
195
+ this.log(chalk.red.bold('Installation failed.'));
196
+ if (result.PhasesFailed.length > 0) {
197
+ this.log(chalk.red(` Failed phase(s): ${result.PhasesFailed.join(', ')}`));
367
198
  }
199
+ this.log(chalk.dim(` Duration: ${this.formatDuration(result.DurationMs)}`));
200
+ this.log('');
201
+ this.log(chalk.yellow('Run "mj install" to resume from the last checkpoint.'));
202
+ this.log(chalk.yellow('Run "mj install --no-resume" to start fresh.'));
203
+ this.log(chalk.yellow('Run "mj doctor" to diagnose issues.'));
204
+ this.error('Installation failed', { exit: 1 });
368
205
  }
369
- /**
370
- * Updates newUserSetup in the mj.config.cjs file.
371
- * @param {string} userName - The new UserName to set.
372
- * @param {string} firstName - The new FirstName to set.
373
- * @param {string} lastName - The new LastName to set.
374
- * @param {string} email - The new Email to set.
375
- */
376
- async updateConfigNewUserSetup(userName, firstName, lastName, email) {
377
- try {
378
- // Read the mj.config.cjs file
379
- const configFileContent = await fs.readFile('mj.config.cjs', 'utf8');
380
- // Parse the content into an AST
381
- const ast = recast.parse(configFileContent);
382
- // Modify the AST
383
- const n = recast.types.namedTypes;
384
- const b = recast.types.builders;
385
- recast.types.visit(ast, {
386
- visitObjectExpression(path) {
387
- const properties = path.node.properties;
388
- // Check if newUserSetup key exists
389
- const newUserSetupProperty = properties.find((prop) => n.Property.check(prop) && n.Identifier.check(prop.key) && prop.key.name === 'newUserSetup');
390
- const newUserSetupValue = b.objectExpression([
391
- b.property('init', b.identifier('userName'), b.literal(userName || '')),
392
- b.property('init', b.identifier('firstName'), b.literal(firstName || '')),
393
- b.property('init', b.identifier('lastName'), b.literal(lastName || '')),
394
- b.property('init', b.identifier('email'), b.literal(email || '')),
395
- ]);
396
- if (newUserSetupProperty && newUserSetupProperty.type === 'Property') {
397
- // Overwrite the existing newUserSetup key with an object
398
- newUserSetupProperty.value = newUserSetupValue;
399
- }
400
- else {
401
- // Add a new newUserSetup key
402
- properties.push(b.property('init', b.identifier('newUserSetup'), newUserSetupValue));
403
- }
404
- return false; // Stop traversing this path
405
- },
406
- });
407
- // Serialize the AST back to a string
408
- const updatedConfigFileContent = recast.prettyPrint(ast).code;
409
- // Write the updated content back to the file
410
- await fs.writeFile('mj.config.cjs', updatedConfigFileContent);
411
- this.log(` Updated mj.config.cjs`);
412
- }
413
- catch (err) {
414
- this.logToStderr('Error:', err);
415
- }
206
+ formatDuration(ms) {
207
+ if (ms < 1000)
208
+ return `${ms}ms`;
209
+ const seconds = Math.floor(ms / 1000);
210
+ if (seconds < 60)
211
+ return `${seconds}s`;
212
+ const minutes = Math.floor(seconds / 60);
213
+ const remainingSeconds = seconds % 60;
214
+ return `${minutes}m ${remainingSeconds}s`;
416
215
  }
417
216
  }
418
217
  //# sourceMappingURL=index.js.map