@friggframework/devtools 2.0.0--canary.546.74db90f.0 → 2.0.0--canary.545.e7becd9.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.
- package/frigg-cli/README.md +1 -1
- package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
- package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
- package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
- package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
- package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
- package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
- package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
- package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +1 -1
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
- package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
- package/frigg-cli/__tests__/unit/commands/provider-dispatch.test.js +383 -0
- package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
- package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
- package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
- package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
- package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
- package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
- package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
- package/frigg-cli/build-command/index.js +123 -11
- package/frigg-cli/container.js +172 -0
- package/frigg-cli/deploy-command/index.js +83 -1
- package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
- package/frigg-cli/doctor-command/index.js +37 -16
- package/frigg-cli/domain/entities/ApiModule.js +272 -0
- package/frigg-cli/domain/entities/AppDefinition.js +227 -0
- package/frigg-cli/domain/entities/Integration.js +198 -0
- package/frigg-cli/domain/exceptions/DomainException.js +24 -0
- package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
- package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
- package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
- package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
- package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
- package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
- package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
- package/frigg-cli/generate-iam-command.js +21 -1
- package/frigg-cli/index.js +21 -6
- package/frigg-cli/index.test.js +7 -2
- package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
- package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
- package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
- package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
- package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
- package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
- package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
- package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
- package/frigg-cli/init-command/backend-first-handler.js +124 -42
- package/frigg-cli/init-command/index.js +2 -1
- package/frigg-cli/init-command/template-handler.js +13 -3
- package/frigg-cli/install-command/backend-js.js +3 -3
- package/frigg-cli/install-command/environment-variables.js +16 -19
- package/frigg-cli/install-command/environment-variables.test.js +12 -13
- package/frigg-cli/install-command/index.js +14 -9
- package/frigg-cli/install-command/integration-file.js +3 -3
- package/frigg-cli/install-command/validate-package.js +5 -9
- package/frigg-cli/jest.config.js +4 -1
- package/frigg-cli/package-lock.json +16226 -0
- package/frigg-cli/repair-command/index.js +121 -128
- package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
- package/frigg-cli/start-command/index.js +324 -2
- package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
- package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
- package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
- package/frigg-cli/templates/backend/.env.example +62 -0
- package/frigg-cli/templates/backend/.eslintrc.json +12 -0
- package/frigg-cli/templates/backend/.prettierrc +6 -0
- package/frigg-cli/templates/backend/docker-compose.yml +22 -0
- package/frigg-cli/templates/backend/index.js +96 -0
- package/frigg-cli/templates/backend/infrastructure.js +12 -0
- package/frigg-cli/templates/backend/jest.config.js +17 -0
- package/frigg-cli/templates/backend/package.json +50 -0
- package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
- package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
- package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
- package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
- package/frigg-cli/templates/backend/test/setup.js +30 -0
- package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
- package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
- package/frigg-cli/ui-command/index.js +58 -36
- package/frigg-cli/utils/__tests__/provider-helper.test.js +55 -0
- package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
- package/frigg-cli/utils/output.js +382 -0
- package/frigg-cli/utils/provider-helper.js +75 -0
- package/frigg-cli/utils/repo-detection.js +85 -37
- package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
- package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
- package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
- package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
- package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
- package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
- package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
- package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
- package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
- package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
- package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
- package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
- package/infrastructure/create-frigg-infrastructure.js +93 -0
- package/infrastructure/docs/iam-policy-templates.md +1 -1
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +200 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +499 -0
- package/infrastructure/domains/admin-scripts/index.js +5 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
- package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +4 -7
- package/infrastructure/domains/shared/resource-discovery.js +5 -5
- package/infrastructure/domains/shared/types/app-definition.js +21 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
- package/infrastructure/infrastructure-composer.js +2 -0
- package/infrastructure/infrastructure-composer.test.js +2 -2
- package/infrastructure/jest.config.js +16 -0
- package/management-ui/README.md +245 -109
- package/package.json +8 -7
- package/frigg-cli/install-command/logger.js +0 -12
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DockerAdapter - Infrastructure adapter for Docker operations
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to interact with Docker and Docker Compose
|
|
5
|
+
* Used by pre-flight checks to verify database infrastructure is ready
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { exec } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
class DockerAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Check if Docker CLI is installed
|
|
15
|
+
* @returns {Promise<boolean>} True if docker command is available
|
|
16
|
+
*/
|
|
17
|
+
async isDockerInstalled() {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
exec('docker --version', (error) => {
|
|
20
|
+
resolve(!error);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if Docker daemon is running
|
|
27
|
+
* @returns {Promise<boolean>} True if docker daemon is responsive
|
|
28
|
+
*/
|
|
29
|
+
async isDockerRunning() {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
exec('docker info', (error) => {
|
|
32
|
+
resolve(!error);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find docker-compose file in project directory or parent
|
|
39
|
+
* @param {string} projectPath - Path to search in
|
|
40
|
+
* @returns {Promise<string|null>} Path to compose file or null if not found
|
|
41
|
+
*/
|
|
42
|
+
async findDockerComposeFile(projectPath) {
|
|
43
|
+
const composeFileNames = [
|
|
44
|
+
'docker-compose.yml',
|
|
45
|
+
'docker-compose.yaml',
|
|
46
|
+
'compose.yml',
|
|
47
|
+
'compose.yaml'
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Search in project path first
|
|
51
|
+
for (const fileName of composeFileNames) {
|
|
52
|
+
const filePath = path.join(projectPath, fileName);
|
|
53
|
+
if (fs.existsSync(filePath)) {
|
|
54
|
+
return filePath;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Search in parent directory (for backend paths)
|
|
59
|
+
const parentPath = path.dirname(projectPath);
|
|
60
|
+
if (parentPath !== projectPath) {
|
|
61
|
+
for (const fileName of composeFileNames) {
|
|
62
|
+
const filePath = path.join(parentPath, fileName);
|
|
63
|
+
if (fs.existsSync(filePath)) {
|
|
64
|
+
return filePath;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start Docker Compose services
|
|
74
|
+
* @param {string} composePath - Path to docker-compose file
|
|
75
|
+
* @returns {Promise<{success: boolean, error?: string, output?: string}>}
|
|
76
|
+
*/
|
|
77
|
+
async startDockerCompose(composePath) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const cmd = `docker compose -f ${composePath} up -d`;
|
|
80
|
+
const options = { cwd: path.dirname(composePath) };
|
|
81
|
+
|
|
82
|
+
exec(cmd, options, (error, stdout, stderr) => {
|
|
83
|
+
if (error) {
|
|
84
|
+
resolve({
|
|
85
|
+
success: false,
|
|
86
|
+
error: stderr || error.message
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
resolve({
|
|
90
|
+
success: true,
|
|
91
|
+
output: stdout
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stop Docker Compose services
|
|
100
|
+
* @param {string} composePath - Path to docker-compose file
|
|
101
|
+
* @returns {Promise<{success: boolean, error?: string, output?: string}>}
|
|
102
|
+
*/
|
|
103
|
+
async stopDockerCompose(composePath) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const cmd = `docker compose -f ${composePath} down`;
|
|
106
|
+
const options = { cwd: path.dirname(composePath) };
|
|
107
|
+
|
|
108
|
+
exec(cmd, options, (error, stdout, stderr) => {
|
|
109
|
+
if (error) {
|
|
110
|
+
resolve({
|
|
111
|
+
success: false,
|
|
112
|
+
error: stderr || error.message
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
resolve({
|
|
116
|
+
success: true,
|
|
117
|
+
output: stdout
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Start Docker Desktop application
|
|
126
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
127
|
+
*/
|
|
128
|
+
async startDockerDesktop() {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
let cmd;
|
|
131
|
+
const platform = process.platform;
|
|
132
|
+
|
|
133
|
+
if (platform === 'darwin') {
|
|
134
|
+
cmd = 'open -a "Docker Desktop"';
|
|
135
|
+
} else if (platform === 'win32') {
|
|
136
|
+
cmd = 'start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"';
|
|
137
|
+
} else {
|
|
138
|
+
// Linux - try systemctl first, fall back to service
|
|
139
|
+
cmd = 'systemctl start docker || service docker start';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exec(cmd, (error) => {
|
|
143
|
+
if (error) {
|
|
144
|
+
resolve({
|
|
145
|
+
success: false,
|
|
146
|
+
error: error.message
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
resolve({ success: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get list of running Docker Compose services
|
|
157
|
+
* @param {string} composePath - Path to docker-compose file
|
|
158
|
+
* @returns {Promise<string[]>} List of running service names
|
|
159
|
+
*/
|
|
160
|
+
async getDockerComposeServices(composePath) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const cmd = `docker compose -f ${composePath} ps --services --filter "status=running"`;
|
|
163
|
+
const options = { cwd: path.dirname(composePath) };
|
|
164
|
+
|
|
165
|
+
exec(cmd, options, (error, stdout) => {
|
|
166
|
+
if (error) {
|
|
167
|
+
resolve([]);
|
|
168
|
+
} else {
|
|
169
|
+
const services = stdout
|
|
170
|
+
.split('\n')
|
|
171
|
+
.map(s => s.trim())
|
|
172
|
+
.filter(s => s.length > 0);
|
|
173
|
+
resolve(services);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if a specific service is running
|
|
181
|
+
* @param {string} composePath - Path to docker-compose file
|
|
182
|
+
* @param {string} serviceName - Name of the service to check
|
|
183
|
+
* @returns {Promise<boolean>} True if service is running
|
|
184
|
+
*/
|
|
185
|
+
async isServiceRunning(composePath, serviceName) {
|
|
186
|
+
const services = await this.getDockerComposeServices(composePath);
|
|
187
|
+
return services.includes(serviceName);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Wait for Docker to become ready after starting
|
|
192
|
+
* @param {object} options - Wait options
|
|
193
|
+
* @param {number} options.maxAttempts - Maximum number of attempts (default: 30)
|
|
194
|
+
* @param {number} options.intervalMs - Interval between attempts in ms (default: 1000)
|
|
195
|
+
* @returns {Promise<boolean>} True if Docker became ready, false if timed out
|
|
196
|
+
*/
|
|
197
|
+
async waitForDockerReady(options = {}) {
|
|
198
|
+
const { maxAttempts = 30, intervalMs = 1000 } = options;
|
|
199
|
+
|
|
200
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
201
|
+
const isRunning = await this.isDockerRunning();
|
|
202
|
+
if (isRunning) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
await this._sleep(intervalMs);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Wait for LocalStack to be ready by polling health endpoint
|
|
213
|
+
* @param {object} options - Wait options
|
|
214
|
+
* @param {number} options.maxAttempts - Maximum number of attempts (default: 30)
|
|
215
|
+
* @param {number} options.intervalMs - Interval between attempts in ms (default: 2000)
|
|
216
|
+
* @param {string} options.endpoint - LocalStack endpoint (default: http://localhost:4566)
|
|
217
|
+
* @returns {Promise<{ready: boolean, services?: object, error?: string}>}
|
|
218
|
+
*/
|
|
219
|
+
async waitForLocalStack(options = {}) {
|
|
220
|
+
const {
|
|
221
|
+
maxAttempts = 30,
|
|
222
|
+
intervalMs = 2000,
|
|
223
|
+
endpoint = process.env.AWS_ENDPOINT || 'http://localhost:4566'
|
|
224
|
+
} = options;
|
|
225
|
+
|
|
226
|
+
const healthUrl = `${endpoint}/_localstack/health`;
|
|
227
|
+
|
|
228
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
229
|
+
try {
|
|
230
|
+
const result = await this._checkLocalStackHealth(healthUrl);
|
|
231
|
+
if (result.ready) {
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
// Not ready yet, wait and retry
|
|
235
|
+
if (attempt < maxAttempts) {
|
|
236
|
+
await this._sleep(intervalMs);
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Connection failed, wait and retry
|
|
240
|
+
if (attempt < maxAttempts) {
|
|
241
|
+
await this._sleep(intervalMs);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
ready: false,
|
|
248
|
+
error: `LocalStack not ready after ${maxAttempts} attempts`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check LocalStack health endpoint
|
|
254
|
+
* @param {string} healthUrl - URL to health endpoint
|
|
255
|
+
* @returns {Promise<{ready: boolean, services?: object}>}
|
|
256
|
+
*/
|
|
257
|
+
async _checkLocalStackHealth(healthUrl) {
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const http = require('http');
|
|
260
|
+
const url = new URL(healthUrl);
|
|
261
|
+
|
|
262
|
+
const req = http.get({
|
|
263
|
+
hostname: url.hostname,
|
|
264
|
+
port: url.port,
|
|
265
|
+
path: url.pathname,
|
|
266
|
+
timeout: 5000
|
|
267
|
+
}, (res) => {
|
|
268
|
+
let data = '';
|
|
269
|
+
res.on('data', chunk => data += chunk);
|
|
270
|
+
res.on('end', () => {
|
|
271
|
+
try {
|
|
272
|
+
const health = JSON.parse(data);
|
|
273
|
+
// Check if services are running
|
|
274
|
+
// LocalStack returns: { "services": { "sqs": "running", ... } }
|
|
275
|
+
const services = health.services || {};
|
|
276
|
+
const sqsReady = services.sqs === 'running' || services.sqs === 'available';
|
|
277
|
+
|
|
278
|
+
resolve({
|
|
279
|
+
ready: sqsReady,
|
|
280
|
+
services: services
|
|
281
|
+
});
|
|
282
|
+
} catch (e) {
|
|
283
|
+
resolve({ ready: false });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
req.on('error', reject);
|
|
289
|
+
req.on('timeout', () => {
|
|
290
|
+
req.destroy();
|
|
291
|
+
reject(new Error('Timeout'));
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Helper to sleep for specified milliseconds
|
|
298
|
+
* @param {number} ms - Milliseconds to sleep
|
|
299
|
+
* @returns {Promise<void>}
|
|
300
|
+
*/
|
|
301
|
+
_sleep(ms) {
|
|
302
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { DockerAdapter };
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InteractivePromptAdapter - Handles prompts in terminal mode or IPC mode
|
|
3
|
+
*
|
|
4
|
+
* Presentation Layer - Adapts prompt interfaces for different contexts:
|
|
5
|
+
* - Terminal mode: Uses @inquirer/prompts for direct CLI interaction
|
|
6
|
+
* - IPC mode: Outputs JSON to stdout and reads responses from stdin
|
|
7
|
+
* (used when Management UI spawns the CLI process)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { confirm, select, input } = require('@inquirer/prompts');
|
|
11
|
+
const { randomUUID } = require('crypto');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Factory for creating the appropriate prompt adapter
|
|
15
|
+
*/
|
|
16
|
+
class InteractivePromptAdapter {
|
|
17
|
+
/**
|
|
18
|
+
* Create a prompt adapter based on mode
|
|
19
|
+
* @param {object} options - Options
|
|
20
|
+
* @param {string} options.mode - 'terminal' or 'ipc'
|
|
21
|
+
* @returns {TerminalPromptAdapter|IpcPromptAdapter}
|
|
22
|
+
*/
|
|
23
|
+
static create(options = {}) {
|
|
24
|
+
// Check for IPC mode via environment variable or explicit option
|
|
25
|
+
const isIpcMode = options.mode === 'ipc' || process.env.FRIGG_IPC === 'true';
|
|
26
|
+
|
|
27
|
+
if (isIpcMode) {
|
|
28
|
+
return new IpcPromptAdapter();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new TerminalPromptAdapter();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Terminal-based prompt adapter using @inquirer/prompts
|
|
37
|
+
*/
|
|
38
|
+
class TerminalPromptAdapter {
|
|
39
|
+
/**
|
|
40
|
+
* Show a confirmation prompt
|
|
41
|
+
* @param {object} options - Prompt options
|
|
42
|
+
* @param {string} options.message - Prompt message
|
|
43
|
+
* @param {boolean} options.default - Default value
|
|
44
|
+
* @returns {Promise<boolean>}
|
|
45
|
+
*/
|
|
46
|
+
async confirm(options) {
|
|
47
|
+
return confirm({
|
|
48
|
+
message: options.message,
|
|
49
|
+
default: options.default
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Show a selection prompt
|
|
55
|
+
* @param {object} options - Prompt options
|
|
56
|
+
* @param {string} options.message - Prompt message
|
|
57
|
+
* @param {Array} options.choices - Array of {value, name} choices
|
|
58
|
+
* @returns {Promise<string>}
|
|
59
|
+
*/
|
|
60
|
+
async select(options) {
|
|
61
|
+
return select({
|
|
62
|
+
message: options.message,
|
|
63
|
+
choices: options.choices
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Show a text input prompt
|
|
69
|
+
* @param {object} options - Prompt options
|
|
70
|
+
* @param {string} options.message - Prompt message
|
|
71
|
+
* @param {string} options.default - Default value
|
|
72
|
+
* @returns {Promise<string>}
|
|
73
|
+
*/
|
|
74
|
+
async input(options) {
|
|
75
|
+
return input({
|
|
76
|
+
message: options.message,
|
|
77
|
+
default: options.default
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Prompt user to resolve a failed pre-flight check
|
|
83
|
+
* @param {object} check - Pre-flight check result
|
|
84
|
+
* @returns {Promise<{shouldResolve: boolean, composePath?: string}>}
|
|
85
|
+
*/
|
|
86
|
+
async promptForResolution(check) {
|
|
87
|
+
if (!check.canResolve) {
|
|
88
|
+
return { shouldResolve: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const shouldResolve = await this.confirm({
|
|
92
|
+
message: check.resolution.prompt,
|
|
93
|
+
default: true
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const result = { shouldResolve };
|
|
97
|
+
|
|
98
|
+
if (check.resolution.composePath) {
|
|
99
|
+
result.composePath = check.resolution.composePath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* IPC-based prompt adapter for Management UI integration
|
|
108
|
+
* Outputs JSON prompts to stdout, reads responses from stdin
|
|
109
|
+
*/
|
|
110
|
+
class IpcPromptAdapter {
|
|
111
|
+
constructor() {
|
|
112
|
+
this._pendingPrompts = new Map();
|
|
113
|
+
this._stdinBuffer = '';
|
|
114
|
+
this._stdinListenerSetup = false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Setup stdin listener for IPC responses (lazy initialization)
|
|
119
|
+
*/
|
|
120
|
+
_setupStdinListener() {
|
|
121
|
+
if (this._stdinListenerSetup) return;
|
|
122
|
+
this._stdinListenerSetup = true;
|
|
123
|
+
|
|
124
|
+
process.stdin.setEncoding('utf8');
|
|
125
|
+
process.stdin.on('data', (chunk) => {
|
|
126
|
+
this._stdinBuffer += chunk;
|
|
127
|
+
|
|
128
|
+
// Process complete lines
|
|
129
|
+
const lines = this._stdinBuffer.split('\n');
|
|
130
|
+
this._stdinBuffer = lines.pop(); // Keep incomplete line in buffer
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (!line.trim()) continue;
|
|
134
|
+
|
|
135
|
+
const ipcMessage = this._parseIpcMessage(line);
|
|
136
|
+
if (ipcMessage && ipcMessage.type === 'prompt_response') {
|
|
137
|
+
this._resolvePrompt(ipcMessage.requestId, ipcMessage.response);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
process.stdin.on('end', () => {
|
|
143
|
+
// stdin closed - resolve any pending prompts with default/false
|
|
144
|
+
for (const [requestId, resolver] of this._pendingPrompts) {
|
|
145
|
+
resolver(false);
|
|
146
|
+
}
|
|
147
|
+
this._pendingPrompts.clear();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Resume stdin to start receiving data
|
|
151
|
+
process.stdin.resume();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate a unique request ID
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
_generateRequestId() {
|
|
159
|
+
return `prompt-${randomUUID().split('-')[0]}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format IPC output message
|
|
164
|
+
* @param {string} type - Message type
|
|
165
|
+
* @param {object} data - Message data
|
|
166
|
+
* @returns {string} JSON string with newline
|
|
167
|
+
*/
|
|
168
|
+
_formatIpcOutput(type, data) {
|
|
169
|
+
const message = {
|
|
170
|
+
frigg_ipc: type,
|
|
171
|
+
...data
|
|
172
|
+
};
|
|
173
|
+
return JSON.stringify(message) + '\n';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse an IPC message from stdin
|
|
178
|
+
* @param {string} message - Raw message string
|
|
179
|
+
* @returns {object|null} Parsed message or null if not IPC
|
|
180
|
+
*/
|
|
181
|
+
_parseIpcMessage(message) {
|
|
182
|
+
try {
|
|
183
|
+
const parsed = JSON.parse(message);
|
|
184
|
+
if (!parsed.frigg_ipc) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
type: parsed.frigg_ipc,
|
|
189
|
+
requestId: parsed.requestId,
|
|
190
|
+
response: parsed.response
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve a pending prompt with a response
|
|
199
|
+
* @param {string} requestId - Request ID
|
|
200
|
+
* @param {any} response - Response value
|
|
201
|
+
*/
|
|
202
|
+
_resolvePrompt(requestId, response) {
|
|
203
|
+
const resolver = this._pendingPrompts.get(requestId);
|
|
204
|
+
if (resolver) {
|
|
205
|
+
resolver(response);
|
|
206
|
+
this._pendingPrompts.delete(requestId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Handle a response from the Management UI
|
|
212
|
+
* @param {string} requestId - Request ID
|
|
213
|
+
* @param {any} response - Response value
|
|
214
|
+
*/
|
|
215
|
+
handleResponse(requestId, response) {
|
|
216
|
+
this._resolvePrompt(requestId, response);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Show a confirmation prompt via IPC
|
|
221
|
+
* @param {object} options - Prompt options
|
|
222
|
+
* @returns {Promise<boolean>}
|
|
223
|
+
*/
|
|
224
|
+
async confirm(options) {
|
|
225
|
+
// Setup stdin listener before sending prompt
|
|
226
|
+
this._setupStdinListener();
|
|
227
|
+
|
|
228
|
+
const requestId = this._generateRequestId();
|
|
229
|
+
|
|
230
|
+
const output = this._formatIpcOutput('prompt_request', {
|
|
231
|
+
requestId,
|
|
232
|
+
prompt: {
|
|
233
|
+
type: 'confirm',
|
|
234
|
+
message: options.message,
|
|
235
|
+
default: options.default
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
process.stdout.write(output);
|
|
240
|
+
|
|
241
|
+
return new Promise((resolve) => {
|
|
242
|
+
this._pendingPrompts.set(requestId, resolve);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Show a selection prompt via IPC
|
|
248
|
+
* @param {object} options - Prompt options
|
|
249
|
+
* @returns {Promise<string>}
|
|
250
|
+
*/
|
|
251
|
+
async select(options) {
|
|
252
|
+
// Setup stdin listener before sending prompt
|
|
253
|
+
this._setupStdinListener();
|
|
254
|
+
|
|
255
|
+
const requestId = this._generateRequestId();
|
|
256
|
+
|
|
257
|
+
const output = this._formatIpcOutput('prompt_request', {
|
|
258
|
+
requestId,
|
|
259
|
+
prompt: {
|
|
260
|
+
type: 'select',
|
|
261
|
+
message: options.message,
|
|
262
|
+
choices: options.choices
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
process.stdout.write(output);
|
|
267
|
+
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
this._pendingPrompts.set(requestId, resolve);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Show a text input prompt via IPC
|
|
275
|
+
* @param {object} options - Prompt options
|
|
276
|
+
* @returns {Promise<string>}
|
|
277
|
+
*/
|
|
278
|
+
async input(options) {
|
|
279
|
+
// Setup stdin listener before sending prompt
|
|
280
|
+
this._setupStdinListener();
|
|
281
|
+
|
|
282
|
+
const requestId = this._generateRequestId();
|
|
283
|
+
|
|
284
|
+
const output = this._formatIpcOutput('prompt_request', {
|
|
285
|
+
requestId,
|
|
286
|
+
prompt: {
|
|
287
|
+
type: 'input',
|
|
288
|
+
message: options.message,
|
|
289
|
+
default: options.default
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
process.stdout.write(output);
|
|
294
|
+
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
this._pendingPrompts.set(requestId, resolve);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Prompt user to resolve a failed pre-flight check via IPC
|
|
302
|
+
* @param {object} check - Pre-flight check result
|
|
303
|
+
* @returns {Promise<{shouldResolve: boolean, composePath?: string}>}
|
|
304
|
+
*/
|
|
305
|
+
async promptForResolution(check) {
|
|
306
|
+
if (!check.canResolve) {
|
|
307
|
+
return { shouldResolve: false };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const shouldResolve = await this.confirm({
|
|
311
|
+
message: check.resolution.prompt,
|
|
312
|
+
default: true
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const result = { shouldResolve };
|
|
316
|
+
|
|
317
|
+
if (check.resolution.composePath) {
|
|
318
|
+
result.composePath = check.resolution.composePath;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
InteractivePromptAdapter,
|
|
327
|
+
TerminalPromptAdapter,
|
|
328
|
+
IpcPromptAdapter
|
|
329
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Frigg Backend Environment Variables
|
|
2
|
+
# Copy this file to .env and fill in your values
|
|
3
|
+
# Or use the default .env file included with the project
|
|
4
|
+
|
|
5
|
+
# ============================================================================
|
|
6
|
+
# Core Configuration
|
|
7
|
+
# ============================================================================
|
|
8
|
+
FRIGG_BASE_URL=http://localhost:3001
|
|
9
|
+
REDIRECT_URI=http://localhost:3000/redirect
|
|
10
|
+
|
|
11
|
+
# Stage (local, dev, test, staging, production)
|
|
12
|
+
STAGE=local
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Database Configuration
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# PostgreSQL (default for new projects - used with Docker)
|
|
18
|
+
DATABASE_URL=postgresql://frigg:frigg@localhost:5432/frigg
|
|
19
|
+
DATABASE_USER=frigg
|
|
20
|
+
DATABASE_PASSWORD=frigg
|
|
21
|
+
|
|
22
|
+
# MongoDB (alternative - uncomment if using MongoDB)
|
|
23
|
+
# MONGO_URI=mongodb://127.0.0.1:27017/frigg
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Security Keys
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# Admin API key for management UI auto-connect (REQUIRED for admin features)
|
|
29
|
+
# Generate a secure random string for production
|
|
30
|
+
FRIGG_ADMIN_API_KEY=dev-admin-key-change-in-production
|
|
31
|
+
|
|
32
|
+
# Shared secret API key for header-based authentication (optional)
|
|
33
|
+
# Used when userManagementMode.sharedSecretEnabled is true
|
|
34
|
+
FRIGG_API_KEY=dev-shared-secret-change-in-production
|
|
35
|
+
|
|
36
|
+
# Health check API key (optional - for protected health endpoints)
|
|
37
|
+
HEALTH_API_KEY=dev-health-key
|
|
38
|
+
|
|
39
|
+
# ============================================================================
|
|
40
|
+
# Encryption Configuration
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# AES encryption (for local development - simpler setup)
|
|
43
|
+
AES_KEY_ID=local-dev-key
|
|
44
|
+
AES_KEY=32-character-secret-key-here-!!
|
|
45
|
+
|
|
46
|
+
# AWS KMS encryption (for production - comment out AES and use these)
|
|
47
|
+
# KMS_KEY_ARN=arn:aws:kms:us-east-1:123456789012:key/your-key-id
|
|
48
|
+
|
|
49
|
+
# ============================================================================
|
|
50
|
+
# AWS Configuration (for deployment)
|
|
51
|
+
# ============================================================================
|
|
52
|
+
AWS_REGION=us-east-1
|
|
53
|
+
# S3_BUCKET_NAME=your-s3-bucket
|
|
54
|
+
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# Integration Credentials
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# Add your integration-specific credentials below
|
|
59
|
+
# HUBSPOT_CLIENT_ID=
|
|
60
|
+
# HUBSPOT_CLIENT_SECRET=
|
|
61
|
+
# SALESFORCE_CLIENT_ID=
|
|
62
|
+
# SALESFORCE_CLIENT_SECRET=
|