@friggframework/devtools 2.0.0--canary.395.495dc7d.0 → 2.0.0--canary.395.ada1d9d.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.
@@ -15,7 +15,6 @@
15
15
  const mockValidator = {
16
16
  validateDatabaseUrl: jest.fn(),
17
17
  getDatabaseType: jest.fn(),
18
- testDatabaseConnection: jest.fn(),
19
18
  checkPrismaClientGenerated: jest.fn()
20
19
  };
21
20
 
@@ -36,21 +35,26 @@ describe('startCommand', () => {
36
35
  let mockProcessExit;
37
36
 
38
37
  beforeEach(() => {
39
- // Mock process.exit BEFORE clearAllMocks to prevent actual exits
40
- mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
38
+ // Mock process.exit to throw error and stop execution (prevents actual exits)
39
+ const exitError = new Error('process.exit called');
40
+ exitError.code = 'PROCESS_EXIT';
41
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
42
+ throw exitError;
43
+ });
41
44
 
42
45
  // Reset mocks
43
46
  jest.clearAllMocks();
44
47
 
45
48
  // Re-apply process.exit mock after clearAllMocks
46
- mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
49
+ mockProcessExit = jest.spyOn(process, 'exit').mockImplementation(() => {
50
+ throw exitError;
51
+ });
47
52
 
48
53
  // Set up default database validator mocks for all tests
49
54
  const defaultValidator = createMockDatabaseValidator();
50
- mockValidator.validateDatabaseUrl.mockImplementation(defaultValidator.validateDatabaseUrl);
51
- mockValidator.getDatabaseType.mockImplementation(defaultValidator.getDatabaseType);
52
- mockValidator.testDatabaseConnection.mockImplementation(defaultValidator.testDatabaseConnection);
53
- mockValidator.checkPrismaClientGenerated.mockImplementation(defaultValidator.checkPrismaClientGenerated);
55
+ mockValidator.validateDatabaseUrl.mockReturnValue(defaultValidator.validateDatabaseUrl());
56
+ mockValidator.getDatabaseType.mockReturnValue(defaultValidator.getDatabaseType());
57
+ mockValidator.checkPrismaClientGenerated.mockReturnValue(defaultValidator.checkPrismaClientGenerated());
54
58
 
55
59
  // Mock dotenv
56
60
  dotenv.config = jest.fn();
@@ -207,7 +211,6 @@ describe('startCommand', () => {
207
211
 
208
212
  expect(mockValidator.validateDatabaseUrl).toHaveBeenCalled();
209
213
  expect(mockValidator.getDatabaseType).toHaveBeenCalled();
210
- expect(mockValidator.testDatabaseConnection).toHaveBeenCalled();
211
214
  expect(mockValidator.checkPrismaClientGenerated).toHaveBeenCalled();
212
215
  expect(mockProcessExit).not.toHaveBeenCalled();
213
216
  expect(spawn).toHaveBeenCalled();
@@ -219,7 +222,7 @@ describe('startCommand', () => {
219
222
  error: 'DATABASE_URL not found'
220
223
  });
221
224
 
222
- await startCommand({});
225
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
223
226
 
224
227
  expect(mockConsoleError).toHaveBeenCalled();
225
228
  expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -231,20 +234,7 @@ describe('startCommand', () => {
231
234
  error: 'Database not configured'
232
235
  });
233
236
 
234
- await startCommand({});
235
-
236
- expect(mockConsoleError).toHaveBeenCalled();
237
- expect(mockProcessExit).toHaveBeenCalledWith(1);
238
- expect(spawn).not.toHaveBeenCalled();
239
- });
240
-
241
- it('should fail when database connection fails', async () => {
242
- mockValidator.testDatabaseConnection.mockResolvedValue({
243
- connected: false,
244
- error: 'Connection failed'
245
- });
246
-
247
- await startCommand({});
237
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
248
238
 
249
239
  expect(mockConsoleError).toHaveBeenCalled();
250
240
  expect(mockProcessExit).toHaveBeenCalledWith(1);
@@ -257,7 +247,7 @@ describe('startCommand', () => {
257
247
  error: 'Client not found'
258
248
  });
259
249
 
260
- await startCommand({});
250
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
261
251
 
262
252
  expect(mockConsoleError).toHaveBeenCalled();
263
253
  expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
@@ -271,7 +261,7 @@ describe('startCommand', () => {
271
261
  error: 'Client not generated'
272
262
  });
273
263
 
274
- await startCommand({});
264
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
275
265
 
276
266
  expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('frigg db:setup'));
277
267
  });
@@ -281,7 +271,7 @@ describe('startCommand', () => {
281
271
  valid: false
282
272
  });
283
273
 
284
- await startCommand({});
274
+ await expect(startCommand({})).rejects.toThrow('process.exit called');
285
275
 
286
276
  expect(mockProcessExit).toHaveBeenCalledWith(1);
287
277
  });
@@ -104,24 +104,28 @@ async function testDatabaseConnection(databaseUrl, dbType, timeout = 5000) {
104
104
 
105
105
  /**
106
106
  * Checks if Prisma client is generated for the database type
107
- * Uses require.resolve() to find the client regardless of package manager hoisting
107
+ * Uses require.resolve to find the client in node_modules
108
108
  *
109
109
  * @param {'mongodb'|'postgresql'} dbType - Database type
110
110
  * @param {string} projectRoot - Project root directory (used for require.resolve context)
111
111
  * @returns {Object} { generated: boolean, path?: string, error?: string }
112
112
  */
113
113
  function checkPrismaClientGenerated(dbType, projectRoot = process.cwd()) {
114
- const clientPackageName = dbType === 'mongodb' ? '@prisma-mongo/client' : '@prisma-postgres/client';
114
+ const clientPackageName = `@prisma-${dbType}/client`;
115
115
 
116
116
  try {
117
- // Use require.resolve to locate the client package
118
- // This works with any package manager (npm, yarn, pnpm) and hoisting strategy
117
+ // First, resolve where @friggframework/core actually is
118
+ // This handles file: dependencies and symlinks correctly
119
+ const corePackagePath = require.resolve('@friggframework/core', {
120
+ paths: [projectRoot]
121
+ });
122
+ const corePackageDir = path.dirname(corePackagePath);
123
+
124
+ // Now look for the Prisma client within the resolved core package
119
125
  const clientPath = require.resolve(clientPackageName, {
120
126
  paths: [
121
- // Try from @friggframework/core first (most likely location)
122
- path.join(projectRoot, 'node_modules', '@friggframework', 'core'),
123
- // Fallback to project root (in case of different hoisting)
124
- projectRoot
127
+ corePackageDir, // Look in the actual core package location
128
+ projectRoot // Fallback to project root
125
129
  ]
126
130
  });
127
131
 
@@ -161,7 +161,7 @@ ${chalk.gray('5.')} Verify network/firewall settings
161
161
  * @returns {string} Formatted error message
162
162
  */
163
163
  function getPrismaClientNotGeneratedError(dbType) {
164
- const clientName = dbType === 'mongodb' ? '@prisma-mongo/client' : '@prisma-postgres/client';
164
+ const clientName = `@prisma-${dbType}/client`;
165
165
 
166
166
  return `
167
167
  ${chalk.red(`❌ Prisma client not generated for ${dbType}`)}
@@ -16,23 +16,27 @@ const chalk = require('chalk');
16
16
  * @throws {Error} If schema file doesn't exist
17
17
  */
18
18
  function getPrismaSchemaPath(dbType, projectRoot = process.cwd()) {
19
- const schemaPath = path.join(
20
- projectRoot,
21
- 'node_modules',
22
- '@friggframework',
23
- 'core',
24
- `prisma-${dbType}`,
25
- 'schema.prisma'
26
- );
27
-
28
- if (!fs.existsSync(schemaPath)) {
29
- throw new Error(
30
- `Prisma schema not found at ${schemaPath}. ` +
31
- 'Ensure @friggframework/core is installed.'
32
- );
19
+ // Try multiple locations for the schema file
20
+ // Priority order:
21
+ // 1. Local node_modules (where @friggframework/core is installed - production scenario)
22
+ // 2. Parent node_modules (workspace/monorepo setup)
23
+ const possiblePaths = [
24
+ // Check where Frigg is installed via npm (production scenario)
25
+ path.join(projectRoot, 'node_modules', '@friggframework', 'core', `prisma-${dbType}`, 'schema.prisma'),
26
+ path.join(projectRoot, '..', 'node_modules', '@friggframework', 'core', `prisma-${dbType}`, 'schema.prisma')
27
+ ];
28
+
29
+ for (const schemaPath of possiblePaths) {
30
+ if (fs.existsSync(schemaPath)) {
31
+ return schemaPath;
32
+ }
33
33
  }
34
34
 
35
- return schemaPath;
35
+ // If not found in any location, throw error
36
+ throw new Error(
37
+ `Prisma schema not found at:\n${possiblePaths.join('\n')}\n\n` +
38
+ 'Ensure @friggframework/core is installed.'
39
+ );
36
40
  }
37
41
 
38
42
  /**
@@ -83,27 +83,45 @@ aws cloudformation update-stack \
83
83
  For custom policy generation based on your app definition:
84
84
 
85
85
  ```javascript
86
- const { generateIAMPolicy, generateIAMCloudFormation } = require('./iam-generator');
86
+ const { generateIAMPolicy, generateIAMCloudFormation, getFeatureSummary } = require('./iam-generator');
87
87
 
88
88
  // Generate basic JSON policy
89
89
  const basicPolicy = generateIAMPolicy('basic');
90
90
 
91
- // Generate full JSON policy
91
+ // Generate full JSON policy
92
92
  const fullPolicy = generateIAMPolicy('full');
93
93
 
94
- // Generate CloudFormation template with auto-detection
95
- const autoTemplate = generateIAMCloudFormation(appDefinition, { mode: 'auto' });
96
-
97
- // Generate CloudFormation template with specific mode
98
- const basicTemplate = generateIAMCloudFormation(appDefinition, { mode: 'basic' });
99
- const fullTemplate = generateIAMCloudFormation(appDefinition, { mode: 'full' });
94
+ // Generate CloudFormation template with auto-detected features
95
+ const summary = getFeatureSummary(appDefinition);
96
+ const template = generateIAMCloudFormation({
97
+ appName: summary.appName,
98
+ features: summary.features,
99
+ userPrefix: 'frigg-deployment-user',
100
+ stackName: 'frigg-deployment-iam'
101
+ });
102
+
103
+ // Or manually specify features
104
+ const customTemplate = generateIAMCloudFormation({
105
+ appName: 'my-app',
106
+ features: {
107
+ vpc: true,
108
+ kms: true,
109
+ ssm: true,
110
+ websockets: false
111
+ },
112
+ userPrefix: 'my-deployment-user',
113
+ stackName: 'my-deployment-stack'
114
+ });
100
115
  ```
101
116
 
102
- ### Generator Modes
117
+ ### Feature Detection
118
+
119
+ Use `getFeatureSummary(appDefinition)` to automatically detect features from your app definition:
103
120
 
104
- - **`basic`** - Core permissions only, ignores app definition features
105
- - **`full`** - All features enabled, ignores app definition features
106
- - **`auto`** - Analyzes app definition and enables features as needed (default)
121
+ ```javascript
122
+ const summary = getFeatureSummary(appDefinition);
123
+ // Returns: { appName, features: { core, vpc, kms, ssm, websockets }, integrationCount }
124
+ ```
107
125
 
108
126
  ## Security Best Practices
109
127
 
@@ -1,52 +1,31 @@
1
1
  const path = require('path');
2
2
 
3
3
  /**
4
- * Generate IAM CloudFormation template based on AppDefinition
5
- * @param {Object} appDefinition - Application definition object
4
+ * Generate IAM CloudFormation template
6
5
  * @param {Object} options - Generation options
7
- * @param {string} [options.deploymentUserName='frigg-deployment-user'] - IAM user name
6
+ * @param {string} [options.appName='Frigg'] - Application name
7
+ * @param {Object} [options.features={}] - Enabled features { vpc, kms, ssm, websockets }
8
+ * @param {string} [options.userPrefix='frigg-deployment-user'] - IAM user name prefix
8
9
  * @param {string} [options.stackName='frigg-deployment-iam'] - CloudFormation stack name
9
- * @param {string} [options.mode='auto'] - Policy mode: 'basic', 'full', or 'auto' (auto-detect from appDefinition)
10
10
  * @returns {string} CloudFormation YAML template
11
11
  */
12
- function generateIAMCloudFormation(appDefinition, options = {}) {
13
- const { deploymentUserName = 'frigg-deployment-user', mode = 'auto' } =
14
- options;
15
-
16
- // Determine which features are enabled based on mode
17
- let features;
18
- if (mode === 'basic') {
19
- features = {
20
- vpc: false,
21
- kms: false,
22
- ssm: false,
23
- websockets: appDefinition.websockets?.enable === true,
24
- };
25
- } else if (mode === 'full') {
26
- features = {
27
- vpc: true,
28
- kms: true,
29
- ssm: true,
30
- websockets: appDefinition.websockets?.enable === true,
31
- };
32
- } else {
33
- // mode === 'auto'
34
- features = {
35
- vpc: appDefinition.vpc?.enable === true,
36
- kms:
37
- appDefinition.encryption
38
- ?.fieldLevelEncryptionMethod === 'kms',
39
- ssm: appDefinition.ssm?.enable === true,
40
- websockets: appDefinition.websockets?.enable === true,
41
- };
42
- }
12
+ function generateIAMCloudFormation(options = {}) {
13
+ const {
14
+ appName = 'Frigg',
15
+ features = {},
16
+ userPrefix = 'frigg-deployment-user',
17
+ stackName = 'frigg-deployment-iam'
18
+ } = options;
19
+
20
+ const deploymentUserName = userPrefix;
21
+
22
+ // Features are already analyzed by caller (use getFeatureSummary to extract features from appDefinition)
23
+ // Expected features: { vpc, kms, ssm, websockets }
43
24
 
44
25
  // Build the CloudFormation template
45
26
  const template = {
46
27
  AWSTemplateFormatVersion: '2010-09-09',
47
- Description: `IAM roles and policies for ${
48
- appDefinition.name || 'Frigg'
49
- } application deployment pipeline`,
28
+ Description: `IAM roles and policies for ${appName} application deployment pipeline`,
50
29
 
51
30
  Parameters: {
52
31
  DeploymentUserName: {
@@ -829,6 +808,7 @@ function generateIAMPolicy(mode = 'basic') {
829
808
 
830
809
  module.exports = {
831
810
  generateIAMCloudFormation,
811
+ generateCloudFormationTemplate: generateIAMCloudFormation, // Alias for generate-command/index.js compatibility
832
812
  getFeatureSummary,
833
813
  generateBasicIAMPolicy,
834
814
  generateFullIAMPolicy,
@@ -51,7 +51,11 @@ describe('IAM Generator', () => {
51
51
  websockets: { enable: false }
52
52
  };
53
53
 
54
- const yaml = generateIAMCloudFormation(appDefinition);
54
+ const summary = getFeatureSummary(appDefinition);
55
+ const yaml = generateIAMCloudFormation({
56
+ appName: summary.appName,
57
+ features: summary.features
58
+ });
55
59
 
56
60
  expect(yaml).toContain('AWSTemplateFormatVersion');
57
61
  expect(yaml).toContain('FriggDeploymentUser');
@@ -66,7 +70,11 @@ describe('IAM Generator', () => {
66
70
  vpc: { enable: true }
67
71
  };
68
72
 
69
- const yaml = generateIAMCloudFormation(appDefinition);
73
+ const summary = getFeatureSummary(appDefinition);
74
+ const yaml = generateIAMCloudFormation({
75
+ appName: summary.appName,
76
+ features: summary.features
77
+ });
70
78
 
71
79
  expect(yaml).toContain('FriggVPCPolicy');
72
80
  expect(yaml).toContain('CreateVPCPermissions');
@@ -81,7 +89,11 @@ describe('IAM Generator', () => {
81
89
  encryption: { fieldLevelEncryptionMethod: 'kms' }
82
90
  };
83
91
 
84
- const yaml = generateIAMCloudFormation(appDefinition);
92
+ const summary = getFeatureSummary(appDefinition);
93
+ const yaml = generateIAMCloudFormation({
94
+ appName: summary.appName,
95
+ features: summary.features
96
+ });
85
97
 
86
98
  expect(yaml).toContain('FriggKMSPolicy');
87
99
  expect(yaml).toContain('CreateKMSPermissions');
@@ -97,7 +109,11 @@ describe('IAM Generator', () => {
97
109
  ssm: { enable: true }
98
110
  };
99
111
 
100
- const yaml = generateIAMCloudFormation(appDefinition);
112
+ const summary = getFeatureSummary(appDefinition);
113
+ const yaml = generateIAMCloudFormation({
114
+ appName: summary.appName,
115
+ features: summary.features
116
+ });
101
117
 
102
118
  expect(yaml).toContain('FriggSSMPolicy');
103
119
  expect(yaml).toContain('CreateSSMPermissions');
@@ -113,7 +129,11 @@ describe('IAM Generator', () => {
113
129
  ssm: { enable: true }
114
130
  };
115
131
 
116
- const yaml = generateIAMCloudFormation(appDefinition);
132
+ const summary = getFeatureSummary(appDefinition);
133
+ const yaml = generateIAMCloudFormation({
134
+ appName: summary.appName,
135
+ features: summary.features
136
+ });
117
137
 
118
138
  // Check parameter defaults match the enabled features
119
139
  expect(yaml).toContain("Default: 'true'"); // VPC enabled
@@ -127,7 +147,11 @@ describe('IAM Generator', () => {
127
147
  integrations: []
128
148
  };
129
149
 
130
- const yaml = generateIAMCloudFormation(appDefinition);
150
+ const summary = getFeatureSummary(appDefinition);
151
+ const yaml = generateIAMCloudFormation({
152
+ appName: summary.appName,
153
+ features: summary.features
154
+ });
131
155
 
132
156
  // Check for core permissions
133
157
  expect(yaml).toContain('cloudformation:CreateStack');
@@ -149,7 +173,11 @@ describe('IAM Generator', () => {
149
173
  integrations: []
150
174
  };
151
175
 
152
- const yaml = generateIAMCloudFormation(appDefinition);
176
+ const summary = getFeatureSummary(appDefinition);
177
+ const yaml = generateIAMCloudFormation({
178
+ appName: summary.appName,
179
+ features: summary.features
180
+ });
153
181
 
154
182
  expect(yaml).toContain('internal-error-queue-*');
155
183
  });
@@ -160,7 +188,11 @@ describe('IAM Generator', () => {
160
188
  integrations: []
161
189
  };
162
190
 
163
- const yaml = generateIAMCloudFormation(appDefinition);
191
+ const summary = getFeatureSummary(appDefinition);
192
+ const yaml = generateIAMCloudFormation({
193
+ appName: summary.appName,
194
+ features: summary.features
195
+ });
164
196
 
165
197
  expect(yaml).toContain('Outputs:');
166
198
  expect(yaml).toContain('DeploymentUserArn:');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.395.495dc7d.0",
4
+ "version": "2.0.0--canary.395.ada1d9d.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -9,8 +9,8 @@
9
9
  "@babel/eslint-parser": "^7.18.9",
10
10
  "@babel/parser": "^7.25.3",
11
11
  "@babel/traverse": "^7.25.3",
12
- "@friggframework/schemas": "2.0.0--canary.395.495dc7d.0",
13
- "@friggframework/test": "2.0.0--canary.395.495dc7d.0",
12
+ "@friggframework/schemas": "2.0.0--canary.395.ada1d9d.0",
13
+ "@friggframework/test": "2.0.0--canary.395.ada1d9d.0",
14
14
  "@hapi/boom": "^10.0.1",
15
15
  "@inquirer/prompts": "^5.3.8",
16
16
  "axios": "^1.7.2",
@@ -32,8 +32,8 @@
32
32
  "serverless-http": "^2.7.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@friggframework/eslint-config": "2.0.0--canary.395.495dc7d.0",
36
- "@friggframework/prettier-config": "2.0.0--canary.395.495dc7d.0",
35
+ "@friggframework/eslint-config": "2.0.0--canary.395.ada1d9d.0",
36
+ "@friggframework/prettier-config": "2.0.0--canary.395.ada1d9d.0",
37
37
  "aws-sdk-client-mock": "^4.1.0",
38
38
  "aws-sdk-client-mock-jest": "^4.1.0",
39
39
  "jest": "^30.1.3",
@@ -68,5 +68,5 @@
68
68
  "publishConfig": {
69
69
  "access": "public"
70
70
  },
71
- "gitHead": "495dc7d533e5d12c04e51dc04cb696f372682001"
71
+ "gitHead": "ada1d9dff6aa44abc366cf9b1c5a2b76b1bb9d6c"
72
72
  }
@@ -1,170 +0,0 @@
1
- const { Command } = require('commander');
2
-
3
- /**
4
- * CommandTester - Utility class for testing CLI commands
5
- * Provides a fluent interface for setting up mocks and executing commands
6
- */
7
- class CommandTester {
8
- constructor(commandDefinition) {
9
- this.commandDefinition = commandDefinition;
10
- this.mocks = new Map();
11
- this.originalEnv = process.env;
12
- this.capturedLogs = {
13
- info: [],
14
- error: [],
15
- debug: [],
16
- warn: []
17
- };
18
- }
19
-
20
- /**
21
- * Set up a mock for a module
22
- * @param {string} modulePath - Path to the module to mock
23
- * @param {object} implementation - Mock implementation
24
- * @returns {CommandTester} - Fluent interface
25
- */
26
- mock(modulePath, implementation) {
27
- this.mocks.set(modulePath, implementation);
28
- return this;
29
- }
30
-
31
- /**
32
- * Set environment variables for the test
33
- * @param {object} env - Environment variables to set
34
- * @returns {CommandTester} - Fluent interface
35
- */
36
- withEnv(env) {
37
- process.env = { ...process.env, ...env };
38
- return this;
39
- }
40
-
41
- /**
42
- * Capture console output during test execution
43
- * @returns {CommandTester} - Fluent interface
44
- */
45
- captureOutput() {
46
- const originalConsole = { ...console };
47
-
48
- console.log = (...args) => {
49
- this.capturedLogs.info.push(args.join(' '));
50
- originalConsole.log(...args);
51
- };
52
-
53
- console.error = (...args) => {
54
- this.capturedLogs.error.push(args.join(' '));
55
- originalConsole.error(...args);
56
- };
57
-
58
- console.warn = (...args) => {
59
- this.capturedLogs.warn.push(args.join(' '));
60
- originalConsole.warn(...args);
61
- };
62
-
63
- console.debug = (...args) => {
64
- this.capturedLogs.debug.push(args.join(' '));
65
- originalConsole.debug(...args);
66
- };
67
-
68
- return this;
69
- }
70
-
71
- /**
72
- * Execute the command with given arguments
73
- * @param {string[]} args - Command arguments
74
- * @param {object} options - Command options
75
- * @returns {Promise<object>} - Execution result
76
- */
77
- async execute(args = [], options = {}) {
78
- // Set up mocks
79
- for (const [path, impl] of this.mocks) {
80
- jest.mock(path, () => impl, { virtual: true });
81
- }
82
-
83
- try {
84
- const program = new Command();
85
-
86
- // Set up the command
87
- const cmd = program
88
- .command(this.commandDefinition.name)
89
- .description(this.commandDefinition.description);
90
-
91
- // Add options if defined
92
- if (this.commandDefinition.options) {
93
- this.commandDefinition.options.forEach(option => {
94
- cmd.option(option.flags, option.description, option.defaultValue);
95
- });
96
- }
97
-
98
- // Add action
99
- cmd.action(this.commandDefinition.action);
100
-
101
- // Mock process.exit to prevent actual exit
102
- const originalExit = process.exit;
103
- let exitCode = 0;
104
- process.exit = (code) => {
105
- exitCode = code;
106
- throw new Error(`Process exited with code ${code}`);
107
- };
108
-
109
- try {
110
- await program.parseAsync(['node', 'cli', ...args]);
111
-
112
- return {
113
- success: true,
114
- exitCode: 0,
115
- logs: this.capturedLogs,
116
- args,
117
- options
118
- };
119
- } catch (error) {
120
- if (error.message.includes('Process exited with code')) {
121
- return {
122
- success: false,
123
- exitCode,
124
- error: error.message,
125
- logs: this.capturedLogs,
126
- args,
127
- options
128
- };
129
- }
130
- throw error;
131
- } finally {
132
- process.exit = originalExit;
133
- }
134
- } finally {
135
- // Clean up mocks
136
- for (const [path] of this.mocks) {
137
- jest.unmock(path);
138
- }
139
-
140
- // Restore environment
141
- process.env = this.originalEnv;
142
- }
143
- }
144
-
145
- /**
146
- * Get captured logs
147
- * @returns {object} - Captured logs by type
148
- */
149
- getLogs() {
150
- return this.capturedLogs;
151
- }
152
-
153
- /**
154
- * Reset the tester state
155
- * @returns {CommandTester} - Fluent interface
156
- */
157
- reset() {
158
- this.mocks.clear();
159
- this.capturedLogs = {
160
- info: [],
161
- error: [],
162
- debug: [],
163
- warn: []
164
- };
165
- process.env = this.originalEnv;
166
- return this;
167
- }
168
- }
169
-
170
- module.exports = { CommandTester };