@friggframework/devtools 2.0.0--canary.549.70ef06a.0 → 2.0.0--canary.545.ccb5010.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 (127) hide show
  1. package/frigg-cli/README.md +1 -1
  2. package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
  3. package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
  4. package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
  5. package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
  6. package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
  7. package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
  8. package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
  9. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
  10. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
  11. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
  12. package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
  13. package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
  14. package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
  15. package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
  16. package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
  17. package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
  18. package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
  19. package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
  20. package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
  21. package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
  22. package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
  23. package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
  24. package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
  25. package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
  26. package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
  27. package/frigg-cli/build-command/index.js +123 -11
  28. package/frigg-cli/container.js +172 -0
  29. package/frigg-cli/deploy-command/index.js +83 -1
  30. package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
  31. package/frigg-cli/doctor-command/index.js +37 -16
  32. package/frigg-cli/domain/entities/ApiModule.js +272 -0
  33. package/frigg-cli/domain/entities/AppDefinition.js +227 -0
  34. package/frigg-cli/domain/entities/Integration.js +198 -0
  35. package/frigg-cli/domain/exceptions/DomainException.js +24 -0
  36. package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
  37. package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
  38. package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
  39. package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
  40. package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
  41. package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
  42. package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
  43. package/frigg-cli/generate-iam-command.js +21 -1
  44. package/frigg-cli/index.js +21 -6
  45. package/frigg-cli/index.test.js +7 -2
  46. package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
  47. package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
  48. package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
  49. package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
  50. package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
  51. package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
  52. package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
  53. package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
  54. package/frigg-cli/init-command/backend-first-handler.js +124 -42
  55. package/frigg-cli/init-command/index.js +2 -1
  56. package/frigg-cli/init-command/template-handler.js +13 -3
  57. package/frigg-cli/install-command/backend-js.js +3 -3
  58. package/frigg-cli/install-command/environment-variables.js +16 -19
  59. package/frigg-cli/install-command/environment-variables.test.js +12 -13
  60. package/frigg-cli/install-command/index.js +14 -9
  61. package/frigg-cli/install-command/integration-file.js +3 -3
  62. package/frigg-cli/install-command/validate-package.js +5 -9
  63. package/frigg-cli/jest.config.js +4 -1
  64. package/frigg-cli/package-lock.json +16226 -0
  65. package/frigg-cli/repair-command/index.js +121 -128
  66. package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
  67. package/frigg-cli/start-command/index.js +324 -2
  68. package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
  69. package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
  70. package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
  71. package/frigg-cli/templates/backend/.env.example +62 -0
  72. package/frigg-cli/templates/backend/.eslintrc.json +12 -0
  73. package/frigg-cli/templates/backend/.prettierrc +6 -0
  74. package/frigg-cli/templates/backend/docker-compose.yml +22 -0
  75. package/frigg-cli/templates/backend/index.js +96 -0
  76. package/frigg-cli/templates/backend/infrastructure.js +12 -0
  77. package/frigg-cli/templates/backend/jest.config.js +17 -0
  78. package/frigg-cli/templates/backend/package.json +50 -0
  79. package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
  80. package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
  81. package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
  82. package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
  83. package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
  84. package/frigg-cli/templates/backend/test/setup.js +30 -0
  85. package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
  86. package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
  87. package/frigg-cli/ui-command/index.js +58 -36
  88. package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
  89. package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
  90. package/frigg-cli/utils/output.js +382 -0
  91. package/frigg-cli/utils/provider-helper.js +75 -0
  92. package/frigg-cli/utils/repo-detection.js +85 -37
  93. package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
  94. package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
  95. package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
  96. package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
  97. package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
  98. package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
  99. package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
  100. package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
  101. package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
  102. package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
  103. package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
  104. package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
  105. package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
  106. package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
  107. package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
  108. package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +145 -0
  109. package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
  110. package/infrastructure/create-frigg-infrastructure.js +93 -0
  111. package/infrastructure/docs/iam-policy-templates.md +1 -1
  112. package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
  113. package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
  114. package/infrastructure/domains/admin-scripts/index.js +5 -0
  115. package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
  116. package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
  117. package/infrastructure/domains/shared/resource-discovery.js +5 -5
  118. package/infrastructure/domains/shared/types/app-definition.js +35 -0
  119. package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
  120. package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
  121. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
  122. package/infrastructure/infrastructure-composer.js +2 -0
  123. package/infrastructure/infrastructure-composer.test.js +2 -2
  124. package/infrastructure/jest.config.js +16 -0
  125. package/management-ui/README.md +245 -109
  126. package/package.json +8 -7
  127. package/frigg-cli/install-command/logger.js +0 -12
@@ -207,7 +207,7 @@ describe('CLI Command: build', () => {
207
207
  await buildCommand({ stage: 'dev' });
208
208
 
209
209
  expect(consoleLogSpy).toHaveBeenCalledWith('Building the serverless application...');
210
- expect(consoleLogSpy).toHaveBeenCalledWith('📦 Packaging serverless application...');
210
+ expect(consoleLogSpy).toHaveBeenCalledWith('Packaging serverless application...');
211
211
  });
212
212
 
213
213
  it('should construct complete valid serverless command', async () => {
@@ -3,8 +3,6 @@
3
3
  * Tests stack listing, selection, and health check orchestration
4
4
  */
5
5
 
6
- const { describe, test, expect, jest, beforeEach } = require('@jest/globals');
7
-
8
6
  describe('Doctor Command - Stack Listing and Selection', () => {
9
7
  let mockCloudFormationClient;
10
8
  let mockSelect;
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Tests for the init command and BackendFirstHandler
3
+ * TDD: These tests define the expected behavior for frigg init
4
+ */
5
+
6
+ const path = require('path');
7
+ const fs = require('fs-extra');
8
+
9
+ // Mock dependencies before requiring the modules
10
+ jest.mock('@inquirer/prompts', () => ({
11
+ select: jest.fn(),
12
+ confirm: jest.fn(),
13
+ multiselect: jest.fn()
14
+ }));
15
+
16
+ jest.mock('../../../utils/npm-registry', () => ({
17
+ searchApiModules: jest.fn().mockResolvedValue([]),
18
+ getModulesByType: jest.fn().mockResolvedValue({})
19
+ }));
20
+
21
+ jest.mock('@friggframework/schemas', () => ({
22
+ validateAppDefinition: jest.fn().mockReturnValue({ valid: true, errors: [] }),
23
+ formatErrors: jest.fn().mockReturnValue('')
24
+ }));
25
+
26
+ const { select, confirm, multiselect } = require('@inquirer/prompts');
27
+ const BackendFirstHandler = require('../../../init-command/backend-first-handler');
28
+
29
+ describe('BackendFirstHandler', () => {
30
+ let tempDir;
31
+ let targetPath;
32
+
33
+ beforeEach(async () => {
34
+ // Create a real temporary directory for each test
35
+ tempDir = global.TestHelpers.createTempDir();
36
+ targetPath = path.join(tempDir, 'test-frigg-app');
37
+
38
+ // Reset all mocks
39
+ jest.clearAllMocks();
40
+ });
41
+
42
+ afterEach(async () => {
43
+ // Clean up
44
+ global.TestHelpers.cleanupTempDir(tempDir);
45
+ });
46
+
47
+ describe('constructor', () => {
48
+ test('initializes with target path and options', () => {
49
+ const handler = new BackendFirstHandler(targetPath, { verbose: true });
50
+
51
+ expect(handler.targetPath).toBe(targetPath);
52
+ expect(handler.appName).toBe('test-frigg-app');
53
+ expect(handler.options.verbose).toBe(true);
54
+ });
55
+
56
+ test('sets templates directory correctly', () => {
57
+ const handler = new BackendFirstHandler(targetPath);
58
+
59
+ expect(handler.templatesDir).toContain('templates');
60
+ });
61
+ });
62
+
63
+ describe('selectDeploymentMode', () => {
64
+ test('returns mode from options if provided', async () => {
65
+ const handler = new BackendFirstHandler(targetPath, { mode: 'standalone' });
66
+
67
+ const mode = await handler.selectDeploymentMode();
68
+
69
+ expect(mode).toBe('standalone');
70
+ expect(select).not.toHaveBeenCalled();
71
+ });
72
+
73
+ test('prompts user if mode not provided', async () => {
74
+ select.mockResolvedValue('embedded');
75
+ const handler = new BackendFirstHandler(targetPath, {});
76
+
77
+ const mode = await handler.selectDeploymentMode();
78
+
79
+ expect(mode).toBe('embedded');
80
+ expect(select).toHaveBeenCalledWith(expect.objectContaining({
81
+ message: expect.stringContaining('deploy')
82
+ }));
83
+ });
84
+ });
85
+
86
+ describe('getProjectConfiguration', () => {
87
+ test('collects all required configuration options', async () => {
88
+ const handler = new BackendFirstHandler(targetPath, { frontend: false });
89
+
90
+ // Mock all prompts in correct order
91
+ // 1. appPurpose
92
+ select.mockResolvedValueOnce('own-app');
93
+ // 2. needsCustomApiModule (only asked when appPurpose === 'own-app')
94
+ confirm.mockResolvedValueOnce(true);
95
+ // 3. includeIntegrations
96
+ confirm.mockResolvedValueOnce(false);
97
+ // 4. serverlessProvider (only for standalone)
98
+ select.mockResolvedValueOnce('aws');
99
+ // 5. installDependencies
100
+ confirm.mockResolvedValueOnce(true);
101
+ // 6. initializeGit
102
+ confirm.mockResolvedValueOnce(true);
103
+
104
+ const config = await handler.getProjectConfiguration('standalone');
105
+
106
+ expect(config.deploymentMode).toBe('standalone');
107
+ expect(config.appPurpose).toBe('own-app');
108
+ expect(config.needsCustomApiModule).toBe(true);
109
+ expect(config.installDependencies).toBe(true);
110
+ expect(config.initializeGit).toBe(true);
111
+ });
112
+
113
+ test('asks about demo frontend when not disabled', async () => {
114
+ const handler = new BackendFirstHandler(targetPath, { frontend: undefined });
115
+
116
+ // Mock prompts in correct order:
117
+ // 1. appPurpose
118
+ select.mockResolvedValueOnce('exploring');
119
+ // 2. includeIntegrations
120
+ confirm.mockResolvedValueOnce(false);
121
+ // 3. includeDemoFrontend (asked when frontend !== false)
122
+ confirm.mockResolvedValueOnce(true);
123
+ // 4. frontendFramework (asked when includeDemoFrontend is true)
124
+ select.mockResolvedValueOnce('react');
125
+ // 5. demoAuthMode (asked when includeDemoFrontend is true)
126
+ select.mockResolvedValueOnce('mock');
127
+ // 6. serverlessProvider (for standalone mode)
128
+ select.mockResolvedValueOnce('local');
129
+ // 7. installDependencies
130
+ confirm.mockResolvedValueOnce(true);
131
+ // 8. initializeGit
132
+ confirm.mockResolvedValueOnce(true);
133
+
134
+ const config = await handler.getProjectConfiguration('standalone');
135
+
136
+ expect(config.includeDemoFrontend).toBe(true);
137
+ expect(config.frontendFramework).toBe('react');
138
+ expect(config.demoAuthMode).toBe('mock');
139
+ });
140
+
141
+ test('skips demo frontend question when frontend is false', async () => {
142
+ const handler = new BackendFirstHandler(targetPath, { frontend: false });
143
+
144
+ select.mockResolvedValueOnce('exploring') // appPurpose
145
+ .mockResolvedValueOnce('local'); // serverlessProvider
146
+ confirm.mockResolvedValueOnce(false) // includeIntegrations
147
+ .mockResolvedValueOnce(true) // installDependencies
148
+ .mockResolvedValueOnce(true); // initializeGit
149
+
150
+ const config = await handler.getProjectConfiguration('standalone');
151
+
152
+ expect(config.includeDemoFrontend).toBeUndefined();
153
+ });
154
+ });
155
+
156
+ describe('createProject', () => {
157
+ test('creates target directory if it does not exist', async () => {
158
+ const handler = new BackendFirstHandler(targetPath, { force: true });
159
+
160
+ // Create minimal config
161
+ const config = {
162
+ deploymentMode: 'standalone',
163
+ installDependencies: false,
164
+ initializeGit: false,
165
+ serverlessProvider: 'local'
166
+ };
167
+
168
+ // Mock that templates exist
169
+ const templatesDir = handler.templatesDir;
170
+ const backendTemplateDir = path.join(templatesDir, 'backend');
171
+
172
+ // We expect the directory to be created
173
+ await handler.ensureSafeDirectory();
174
+
175
+ expect(fs.existsSync(targetPath)).toBe(true);
176
+ });
177
+
178
+ test('throws error when directory is not empty without force flag', async () => {
179
+ // Create target directory with a file in it
180
+ await fs.ensureDir(targetPath);
181
+ await fs.writeFile(path.join(targetPath, 'existing-file.js'), 'content');
182
+
183
+ const handler = new BackendFirstHandler(targetPath, { force: false });
184
+
185
+ await expect(handler.ensureSafeDirectory())
186
+ .rejects
187
+ .toThrow('Directory not empty');
188
+ });
189
+
190
+ test('allows non-empty directory with force flag', async () => {
191
+ // Create target directory with a file in it
192
+ await fs.ensureDir(targetPath);
193
+ await fs.writeFile(path.join(targetPath, 'existing-file.js'), 'content');
194
+
195
+ const handler = new BackendFirstHandler(targetPath, { force: true });
196
+
197
+ // Should not throw
198
+ await handler.ensureSafeDirectory();
199
+
200
+ expect(fs.existsSync(targetPath)).toBe(true);
201
+ });
202
+
203
+ test('allows allowed files without force flag', async () => {
204
+ // Create target directory with allowed files
205
+ await fs.ensureDir(targetPath);
206
+ await fs.writeFile(path.join(targetPath, '.git'), '');
207
+ await fs.writeFile(path.join(targetPath, '.gitignore'), '');
208
+ await fs.writeFile(path.join(targetPath, 'README.md'), '');
209
+
210
+ const handler = new BackendFirstHandler(targetPath, { force: false });
211
+
212
+ // Should not throw for allowed files
213
+ await handler.ensureSafeDirectory();
214
+
215
+ expect(fs.existsSync(targetPath)).toBe(true);
216
+ });
217
+ });
218
+
219
+ describe('createStandaloneProject', () => {
220
+ // Note: These tests rely on the real backend template in templates/backend
221
+ // If the template doesn't exist, tests will be skipped
222
+
223
+ test('creates package.json with correct scripts', async () => {
224
+ const handler = new BackendFirstHandler(targetPath, { force: true });
225
+ await fs.ensureDir(targetPath);
226
+
227
+ const config = {
228
+ serverlessProvider: 'aws',
229
+ starterIntegrations: [],
230
+ installDependencies: false
231
+ };
232
+
233
+ await handler.createStandaloneProject(config);
234
+
235
+ const packageJson = await fs.readJSON(path.join(targetPath, 'package.json'));
236
+
237
+ expect(packageJson.name).toBe('test-frigg-app');
238
+ expect(packageJson.scripts).toHaveProperty('start');
239
+ expect(packageJson.scripts).toHaveProperty('build');
240
+ expect(packageJson.scripts).toHaveProperty('deploy');
241
+ expect(packageJson.scripts).toHaveProperty('test');
242
+ });
243
+
244
+ test('adds selected integrations as dependencies', async () => {
245
+ const handler = new BackendFirstHandler(targetPath, { force: true });
246
+ await fs.ensureDir(targetPath);
247
+
248
+ const config = {
249
+ serverlessProvider: 'aws',
250
+ starterIntegrations: ['salesforce', 'hubspot'],
251
+ installDependencies: false
252
+ };
253
+
254
+ await handler.createStandaloneProject(config);
255
+
256
+ const packageJson = await fs.readJSON(path.join(targetPath, 'package.json'));
257
+
258
+ expect(packageJson.dependencies).toHaveProperty('@friggframework/api-module-salesforce');
259
+ expect(packageJson.dependencies).toHaveProperty('@friggframework/api-module-hubspot');
260
+ });
261
+
262
+ test('includes @friggframework/core as dependency', async () => {
263
+ const handler = new BackendFirstHandler(targetPath, { force: true });
264
+ await fs.ensureDir(targetPath);
265
+
266
+ const config = {
267
+ serverlessProvider: 'local',
268
+ starterIntegrations: [],
269
+ installDependencies: false
270
+ };
271
+
272
+ await handler.createStandaloneProject(config);
273
+
274
+ const packageJson = await fs.readJSON(path.join(targetPath, 'package.json'));
275
+
276
+ expect(packageJson.dependencies).toHaveProperty('@friggframework/core');
277
+ });
278
+ });
279
+
280
+ describe('createEmbeddedProject', () => {
281
+ // Note: These tests rely on the real backend template in templates/backend
282
+ // If the template doesn't exist, tests will be skipped
283
+
284
+ test('creates frigg-integration subdirectory', async () => {
285
+ const handler = new BackendFirstHandler(targetPath, { force: true });
286
+ await fs.ensureDir(targetPath);
287
+
288
+ const config = {
289
+ installDependencies: false
290
+ };
291
+
292
+ await handler.createEmbeddedProject(config);
293
+
294
+ const integrationDir = path.join(targetPath, 'frigg-integration');
295
+ expect(fs.existsSync(integrationDir)).toBe(true);
296
+ });
297
+
298
+ test('creates FRIGG_INTEGRATION.md guide', async () => {
299
+ const handler = new BackendFirstHandler(targetPath, { force: true });
300
+ await fs.ensureDir(targetPath);
301
+
302
+ const config = {
303
+ installDependencies: false
304
+ };
305
+
306
+ await handler.createEmbeddedProject(config);
307
+
308
+ const guidePath = path.join(targetPath, 'FRIGG_INTEGRATION.md');
309
+ expect(fs.existsSync(guidePath)).toBe(true);
310
+
311
+ const content = await fs.readFile(guidePath, 'utf8');
312
+ expect(content).toContain('# Frigg Integration Guide');
313
+ expect(content).toContain('@friggframework/core');
314
+ });
315
+ });
316
+
317
+ describe('getIntegrationClassName', () => {
318
+ test('converts known integrations to class names', () => {
319
+ const handler = new BackendFirstHandler(targetPath);
320
+
321
+ expect(handler.getIntegrationClassName('salesforce')).toBe('SalesforceIntegration');
322
+ expect(handler.getIntegrationClassName('hubspot')).toBe('HubSpotIntegration');
323
+ expect(handler.getIntegrationClassName('slack')).toBe('SlackIntegration');
324
+ expect(handler.getIntegrationClassName('google-sheets')).toBe('GoogleSheetsIntegration');
325
+ });
326
+
327
+ test('generates class name for unknown integrations', () => {
328
+ const handler = new BackendFirstHandler(targetPath);
329
+
330
+ expect(handler.getIntegrationClassName('custom-api')).toBe('Custom-apiIntegration');
331
+ });
332
+ });
333
+
334
+ describe('isUsingYarn', () => {
335
+ test('returns true when npm_config_user_agent contains yarn', () => {
336
+ const originalEnv = process.env.npm_config_user_agent;
337
+ process.env.npm_config_user_agent = 'yarn/1.22.0';
338
+
339
+ const handler = new BackendFirstHandler(targetPath);
340
+
341
+ expect(handler.isUsingYarn()).toBe(true);
342
+
343
+ process.env.npm_config_user_agent = originalEnv;
344
+ });
345
+
346
+ test('returns false when using npm', () => {
347
+ const originalEnv = process.env.npm_config_user_agent;
348
+ process.env.npm_config_user_agent = 'npm/8.0.0';
349
+
350
+ const handler = new BackendFirstHandler(targetPath);
351
+
352
+ expect(handler.isUsingYarn()).toBe(false);
353
+
354
+ process.env.npm_config_user_agent = originalEnv;
355
+ });
356
+ });
357
+ });
358
+
359
+ describe('initCommand', () => {
360
+ const { initCommand } = require('../../../init-command');
361
+ let tempDir;
362
+
363
+ beforeEach(() => {
364
+ tempDir = global.TestHelpers.createTempDir();
365
+ jest.clearAllMocks();
366
+ });
367
+
368
+ afterEach(() => {
369
+ global.TestHelpers.cleanupTempDir(tempDir);
370
+ });
371
+
372
+ test('validates project name - uppercase names are allowed in npm', async () => {
373
+ // Note: npm actually allows uppercase names now, they get lowercased
374
+ // The validate-npm-package-name package allows uppercase
375
+ const validName = path.join(tempDir, 'Valid-Name');
376
+
377
+ // Mock prompts to allow the command to proceed
378
+ select.mockResolvedValue('standalone');
379
+ confirm.mockResolvedValue(false);
380
+
381
+ // This should not throw for package name validation
382
+ // It may fail for other reasons like missing templates
383
+ try {
384
+ await initCommand(validName, { mode: 'standalone' });
385
+ } catch (e) {
386
+ // Expected to fail for missing template, not for name validation
387
+ expect(e.message).not.toContain('npm naming restrictions');
388
+ }
389
+ });
390
+
391
+ test('checks Node version', async () => {
392
+ const projectPath = path.join(tempDir, 'valid-project');
393
+
394
+ // Mock prompts to return quickly
395
+ select.mockResolvedValue('standalone');
396
+ confirm.mockResolvedValue(false);
397
+
398
+ // This should not throw for invalid Node version (just warn)
399
+ // The test validates checkNodeVersion is called
400
+ try {
401
+ await initCommand(projectPath, { mode: 'standalone' });
402
+ } catch (e) {
403
+ // May fail for other reasons, but shouldn't throw for Node version
404
+ }
405
+ });
406
+ });
@@ -26,7 +26,7 @@ jest.mock('../../../install-command/validate-package', () => ({
26
26
  validatePackageExists: jest.fn(), // External: npm registry
27
27
  searchAndSelectPackage: jest.fn() // External: interactive selection
28
28
  }));
29
- jest.mock('@friggframework/core', () => ({
29
+ jest.mock('@friggframework/core/utils', () => ({
30
30
  findNearestBackendPackageJson: jest.fn(),
31
31
  validateBackendPath: jest.fn()
32
32
  }));
@@ -43,13 +43,13 @@ const { installPackage } = require('../../../install-command/install-package');
43
43
  const { commitChanges } = require('../../../install-command/commit-changes');
44
44
  const { handleEnvVariables } = require('../../../install-command/environment-variables');
45
45
  const { validatePackageExists, searchAndSelectPackage } = require('../../../install-command/validate-package');
46
- const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core');
46
+ const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core/utils');
47
47
  const { installCommand } = require('../../../install-command');
48
+ const output = require('../../../utils/output');
48
49
 
49
50
  describe('CLI Command: install', () => {
50
51
  let processExitSpy;
51
- let consoleLogSpy;
52
- let consoleErrorSpy;
52
+
53
53
  const mockBackendPath = '/mock/backend/package.json';
54
54
  const mockBackendDir = '/mock/backend';
55
55
 
@@ -59,9 +59,14 @@ describe('CLI Command: install', () => {
59
59
  // Mock process.exit to prevent actual exit
60
60
  processExitSpy = jest.spyOn(process, 'exit').mockImplementation();
61
61
 
62
- // Spy on console for logger (don't mock logger - test it!)
63
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
64
- consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
62
+ // Mock output module
63
+ output.success = jest.fn();
64
+ output.error = jest.fn();
65
+ output.spinner = jest.fn().mockReturnValue({
66
+ start: jest.fn(),
67
+ succeed: jest.fn(),
68
+ fail: jest.fn()
69
+ });
65
70
 
66
71
  // Setup fs-extra mocks - Let Frigg code run, just mock I/O
67
72
  fs.ensureDirSync = jest.fn();
@@ -98,8 +103,7 @@ describe('CLI Command: install', () => {
98
103
 
99
104
  afterEach(() => {
100
105
  processExitSpy.mockRestore();
101
- consoleLogSpy.mockRestore();
102
- consoleErrorSpy.mockRestore();
106
+
103
107
  jest.resetModules(); // Clear module cache after each test
104
108
  });
105
109
 
@@ -205,9 +209,9 @@ describe('CLI Command: install', () => {
205
209
  it('should log info messages during installation', async () => {
206
210
  await installCommand('slack');
207
211
 
208
- // Verify logger actually logged (we spy on console)
209
- expect(consoleLogSpy).toHaveBeenCalledWith(
210
- expect.stringContaining('Successfully installed @friggframework/api-module-slack')
212
+ // Verify output.spinner was used for installation progress
213
+ expect(output.spinner).toHaveBeenCalledWith(
214
+ expect.stringContaining('Installing integration for Slack')
211
215
  );
212
216
  });
213
217
 
@@ -334,8 +338,8 @@ describe('CLI Command: install', () => {
334
338
 
335
339
  await installCommand('slack');
336
340
 
337
- // Verify error logged via console.error (we spy on it)
338
- expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
341
+ // Verify error logged via output.error
342
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
339
343
  expect(processExitSpy).toHaveBeenCalledWith(1);
340
344
  });
341
345
 
@@ -345,7 +349,7 @@ describe('CLI Command: install', () => {
345
349
 
346
350
  await installCommand('slack');
347
351
 
348
- expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
352
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
349
353
  expect(processExitSpy).toHaveBeenCalledWith(1);
350
354
  });
351
355
 
@@ -357,7 +361,7 @@ describe('CLI Command: install', () => {
357
361
 
358
362
  await installCommand('slack');
359
363
 
360
- expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
364
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
361
365
  expect(processExitSpy).toHaveBeenCalledWith(1);
362
366
  });
363
367
 
@@ -370,7 +374,7 @@ describe('CLI Command: install', () => {
370
374
 
371
375
  await installCommand('slack');
372
376
 
373
- expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
377
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
374
378
  expect(processExitSpy).toHaveBeenCalledWith(1);
375
379
  });
376
380
 
@@ -383,7 +387,7 @@ describe('CLI Command: install', () => {
383
387
 
384
388
  await installCommand('slack');
385
389
 
386
- expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
390
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', expect.any(Error));
387
391
  expect(processExitSpy).toHaveBeenCalledWith(1);
388
392
  });
389
393
 
@@ -393,7 +397,7 @@ describe('CLI Command: install', () => {
393
397
 
394
398
  await installCommand('slack');
395
399
 
396
- expect(consoleErrorSpy).toHaveBeenCalledWith('An error occurred:', error);
400
+ expect(output.error).toHaveBeenCalledWith('An error occurred:', error);
397
401
  expect(processExitSpy).toHaveBeenCalledWith(1);
398
402
  });
399
403
  });