@friggframework/devtools 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.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/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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnitOfWork
|
|
3
|
+
* Coordinates transactions across repositories
|
|
4
|
+
*/
|
|
5
|
+
class UnitOfWork {
|
|
6
|
+
constructor(fileSystemAdapter) {
|
|
7
|
+
this.fileSystemAdapter = fileSystemAdapter;
|
|
8
|
+
this.repositories = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a repository
|
|
13
|
+
*/
|
|
14
|
+
registerRepository(name, repository) {
|
|
15
|
+
this.repositories.set(name, repository);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Commit all tracked operations
|
|
21
|
+
*/
|
|
22
|
+
async commit() {
|
|
23
|
+
try {
|
|
24
|
+
await this.fileSystemAdapter.commit();
|
|
25
|
+
return {success: true};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error(`Failed to commit transaction: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rollback all tracked operations
|
|
33
|
+
*/
|
|
34
|
+
async rollback() {
|
|
35
|
+
return await this.fileSystemAdapter.rollback();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Clear tracked operations without commit/rollback
|
|
40
|
+
*/
|
|
41
|
+
clear() {
|
|
42
|
+
this.fileSystemAdapter.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {UnitOfWork};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BackendJsUpdater
|
|
6
|
+
*
|
|
7
|
+
* Infrastructure service for updating backend.js file with new integrations
|
|
8
|
+
* Uses AST manipulation to safely add integration imports and registrations
|
|
9
|
+
*/
|
|
10
|
+
class BackendJsUpdater {
|
|
11
|
+
constructor(fileSystemAdapter, backendPath) {
|
|
12
|
+
this.fileSystemAdapter = fileSystemAdapter;
|
|
13
|
+
this.backendPath = backendPath;
|
|
14
|
+
this.indexJsPath = path.join(backendPath, 'index.js');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register an integration in backend/index.js
|
|
19
|
+
* @param {string} integrationName - kebab-case integration name
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
async registerIntegration(integrationName) {
|
|
23
|
+
if (!await this.fileSystemAdapter.exists(this.indexJsPath)) {
|
|
24
|
+
throw new Error('backend/index.js not found');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const className = this._toClassName(integrationName);
|
|
28
|
+
const importPath = `./src/integrations/${className}Integration`;
|
|
29
|
+
|
|
30
|
+
await this.fileSystemAdapter.updateFile(this.indexJsPath, (content) => {
|
|
31
|
+
return this._addIntegration(content, className, integrationName, importPath);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remove an integration from backend/index.js
|
|
37
|
+
* @param {string} integrationName
|
|
38
|
+
* @returns {Promise<void>}
|
|
39
|
+
*/
|
|
40
|
+
async unregisterIntegration(integrationName) {
|
|
41
|
+
if (!await this.fileSystemAdapter.exists(this.indexJsPath)) {
|
|
42
|
+
throw new Error('backend/index.js not found');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const className = this._toClassName(integrationName);
|
|
46
|
+
|
|
47
|
+
await this.fileSystemAdapter.updateFile(this.indexJsPath, (content) => {
|
|
48
|
+
return this._removeIntegration(content, className, integrationName);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add integration to backend.js content
|
|
54
|
+
* Simple string manipulation approach (can be replaced with AST parsing if needed)
|
|
55
|
+
*
|
|
56
|
+
* @param {string} content - Current backend.js content
|
|
57
|
+
* @param {string} className - Integration class name
|
|
58
|
+
* @param {string} integrationName - kebab-case name
|
|
59
|
+
* @param {string} importPath - relative import path
|
|
60
|
+
* @returns {string} - Updated content
|
|
61
|
+
*/
|
|
62
|
+
_addIntegration(content, className, integrationName, importPath) {
|
|
63
|
+
// Check if integration is already registered
|
|
64
|
+
if (content.includes(`const ${className}Integration`)) {
|
|
65
|
+
console.warn(`Integration ${integrationName} is already registered in backend.js`);
|
|
66
|
+
return content;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let updated = content;
|
|
70
|
+
|
|
71
|
+
// 1. Add import statement after other integration imports
|
|
72
|
+
const importRegex = /(const \w+Integration = require\('\.\/src\/integrations\/[^']+'\);)/g;
|
|
73
|
+
const importMatches = [...content.matchAll(importRegex)];
|
|
74
|
+
|
|
75
|
+
if (importMatches.length > 0) {
|
|
76
|
+
// Add after last integration import
|
|
77
|
+
const lastImport = importMatches[importMatches.length - 1];
|
|
78
|
+
const insertIndex = lastImport.index + lastImport[0].length;
|
|
79
|
+
const importStatement = `\nconst ${className}Integration = require('${importPath}');`;
|
|
80
|
+
updated = updated.slice(0, insertIndex) + importStatement + updated.slice(insertIndex);
|
|
81
|
+
} else {
|
|
82
|
+
// No existing imports - add at the top after requires
|
|
83
|
+
const requiresRegex = /const .+ = require\([^)]+\);/g;
|
|
84
|
+
const requireMatches = [...content.matchAll(requiresRegex)];
|
|
85
|
+
if (requireMatches.length > 0) {
|
|
86
|
+
const lastRequire = requireMatches[requireMatches.length - 1];
|
|
87
|
+
const insertIndex = lastRequire.index + lastRequire[0].length;
|
|
88
|
+
const importStatement = `\n\n// Integrations\nconst ${className}Integration = require('${importPath}');`;
|
|
89
|
+
updated = updated.slice(0, insertIndex) + importStatement + updated.slice(insertIndex);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Add to integrations array
|
|
94
|
+
// Look for patterns:
|
|
95
|
+
// - const integrations = [...]
|
|
96
|
+
// - integrations: [...] (inside appDefinition object)
|
|
97
|
+
|
|
98
|
+
// Try standalone array first
|
|
99
|
+
const standaloneArrayRegex = /const integrations = \[([\s\S]*?)\];/;
|
|
100
|
+
let match = updated.match(standaloneArrayRegex);
|
|
101
|
+
|
|
102
|
+
if (match) {
|
|
103
|
+
const currentArray = match[1];
|
|
104
|
+
const newEntry = `\n ${className}Integration,`;
|
|
105
|
+
|
|
106
|
+
// Check if it's an empty array
|
|
107
|
+
if (currentArray.trim() === '') {
|
|
108
|
+
updated = updated.replace(standaloneArrayRegex, `const integrations = [${newEntry}\n];`);
|
|
109
|
+
} else {
|
|
110
|
+
// Add to existing array
|
|
111
|
+
const insertAt = match.index + match[0].length - 2; // Before ];
|
|
112
|
+
updated = updated.slice(0, insertAt) + ',' + newEntry + updated.slice(insertAt);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// Try appDefinition pattern
|
|
116
|
+
const appDefArrayRegex = /integrations:\s*\[([\s\S]*?)\]/;
|
|
117
|
+
match = updated.match(appDefArrayRegex);
|
|
118
|
+
|
|
119
|
+
if (match) {
|
|
120
|
+
const currentArray = match[1];
|
|
121
|
+
const newEntry = `\n ${className}Integration,`;
|
|
122
|
+
|
|
123
|
+
// Check if array is empty or has only comments
|
|
124
|
+
const hasOnlyComments = currentArray.trim() === '' ||
|
|
125
|
+
currentArray.trim().split('\n').every(line => line.trim().startsWith('//'));
|
|
126
|
+
|
|
127
|
+
if (hasOnlyComments) {
|
|
128
|
+
// Replace entire array content
|
|
129
|
+
updated = updated.replace(appDefArrayRegex, `integrations: [${newEntry}\n ]`);
|
|
130
|
+
} else {
|
|
131
|
+
// Add to existing array - find the last entry and add comma if needed
|
|
132
|
+
const lines = currentArray.split('\n');
|
|
133
|
+
const lastNonEmptyLine = lines.reverse().find(line => line.trim() && !line.trim().startsWith('//'));
|
|
134
|
+
const needsComma = lastNonEmptyLine && !lastNonEmptyLine.trim().endsWith(',');
|
|
135
|
+
const comma = needsComma ? ',' : '';
|
|
136
|
+
|
|
137
|
+
const insertAt = match.index + match[0].length - 1; // Before ]
|
|
138
|
+
updated = updated.slice(0, insertAt) + comma + newEntry + '\n ' + updated.slice(insertAt);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// No integrations array found - this is a problem
|
|
142
|
+
console.warn('Could not find integrations array in backend/index.js');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return updated;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Remove integration from backend.js content
|
|
151
|
+
*
|
|
152
|
+
* @param {string} content
|
|
153
|
+
* @param {string} className
|
|
154
|
+
* @param {string} integrationName
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
_removeIntegration(content, className, integrationName) {
|
|
158
|
+
let updated = content;
|
|
159
|
+
|
|
160
|
+
// 1. Remove import statement
|
|
161
|
+
const importRegex = new RegExp(`\\nconst ${className}Integration = require\\([^)]+\\);`, 'g');
|
|
162
|
+
updated = updated.replace(importRegex, '');
|
|
163
|
+
|
|
164
|
+
// 2. Remove from integrations array
|
|
165
|
+
const arrayEntryRegex = new RegExp(`,?\\s*${className}Integration,?`, 'g');
|
|
166
|
+
updated = updated.replace(arrayEntryRegex, '');
|
|
167
|
+
|
|
168
|
+
// Clean up extra commas
|
|
169
|
+
updated = updated.replace(/,\s*,/g, ',');
|
|
170
|
+
updated = updated.replace(/\[\s*,/g, '[');
|
|
171
|
+
updated = updated.replace(/,\s*\]/g, ']');
|
|
172
|
+
|
|
173
|
+
return updated;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Convert kebab-case to ClassName
|
|
178
|
+
* @param {string} kebabCase
|
|
179
|
+
* @returns {string}
|
|
180
|
+
*/
|
|
181
|
+
_toClassName(kebabCase) {
|
|
182
|
+
return kebabCase
|
|
183
|
+
.split('-')
|
|
184
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
185
|
+
.join('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if backend/index.js exists
|
|
190
|
+
* @returns {Promise<boolean>}
|
|
191
|
+
*/
|
|
192
|
+
async exists() {
|
|
193
|
+
return await this.fileSystemAdapter.exists(this.indexJsPath);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {BackendJsUpdater};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FileSystemAdapter
|
|
6
|
+
* Low-level file system operations with atomic write/update and rollback support
|
|
7
|
+
*/
|
|
8
|
+
class FileSystemAdapter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.operations = []; // Track operations for rollback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Write file atomically (temp file + rename)
|
|
15
|
+
*/
|
|
16
|
+
async writeFile(filePath, content) {
|
|
17
|
+
const tempPath = `${filePath}.tmp.${Date.now()}`;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
21
|
+
await fs.rename(tempPath, filePath);
|
|
22
|
+
|
|
23
|
+
this.operations.push({
|
|
24
|
+
type: 'create',
|
|
25
|
+
path: filePath,
|
|
26
|
+
backup: null
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {success: true, path: filePath};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Clean up temp file on error
|
|
32
|
+
if (await fs.pathExists(tempPath)) {
|
|
33
|
+
await fs.unlink(tempPath);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Failed to write file ${filePath}: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update file atomically (backup + write + rename)
|
|
41
|
+
*/
|
|
42
|
+
async updateFile(filePath, updateFn) {
|
|
43
|
+
const backupPath = `${filePath}.backup.${Date.now()}`;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Create backup if file exists
|
|
47
|
+
if (await fs.pathExists(filePath)) {
|
|
48
|
+
await fs.copy(filePath, backupPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Read current content
|
|
52
|
+
const currentContent = await fs.pathExists(filePath)
|
|
53
|
+
? await fs.readFile(filePath, 'utf-8')
|
|
54
|
+
: '';
|
|
55
|
+
|
|
56
|
+
// Apply update function
|
|
57
|
+
const newContent = await updateFn(currentContent);
|
|
58
|
+
|
|
59
|
+
// Write to temp, then rename (atomic)
|
|
60
|
+
const tempPath = `${filePath}.tmp.${Date.now()}`;
|
|
61
|
+
await fs.writeFile(tempPath, newContent, 'utf-8');
|
|
62
|
+
await fs.rename(tempPath, filePath);
|
|
63
|
+
|
|
64
|
+
this.operations.push({
|
|
65
|
+
type: 'update',
|
|
66
|
+
path: filePath,
|
|
67
|
+
backup: backupPath
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {success: true, path: filePath};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Restore from backup on error
|
|
73
|
+
if (await fs.pathExists(backupPath)) {
|
|
74
|
+
await fs.copy(backupPath, filePath);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Failed to update file ${filePath}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read file content
|
|
82
|
+
*/
|
|
83
|
+
async readFile(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if file or directory exists
|
|
93
|
+
*/
|
|
94
|
+
async exists(filePath) {
|
|
95
|
+
return await fs.pathExists(filePath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Ensure directory exists (create if needed)
|
|
100
|
+
*/
|
|
101
|
+
async ensureDirectory(dirPath) {
|
|
102
|
+
if (!await fs.pathExists(dirPath)) {
|
|
103
|
+
await fs.ensureDir(dirPath);
|
|
104
|
+
|
|
105
|
+
this.operations.push({
|
|
106
|
+
type: 'mkdir',
|
|
107
|
+
path: dirPath,
|
|
108
|
+
backup: null
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {exists: true};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* List directories in a path
|
|
117
|
+
*/
|
|
118
|
+
async listDirectories(dirPath) {
|
|
119
|
+
if (!await fs.pathExists(dirPath)) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const entries = await fs.readdir(dirPath, {withFileTypes: true});
|
|
124
|
+
return entries
|
|
125
|
+
.filter(entry => entry.isDirectory())
|
|
126
|
+
.map(entry => entry.name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* List files in a path (optionally with pattern)
|
|
131
|
+
*/
|
|
132
|
+
async listFiles(dirPath, pattern = null) {
|
|
133
|
+
if (!await fs.pathExists(dirPath)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entries = await fs.readdir(dirPath, {withFileTypes: true});
|
|
138
|
+
let files = entries
|
|
139
|
+
.filter(entry => entry.isFile())
|
|
140
|
+
.map(entry => entry.name);
|
|
141
|
+
|
|
142
|
+
// Apply pattern filter if provided
|
|
143
|
+
if (pattern) {
|
|
144
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
145
|
+
files = files.filter(file => regex.test(file));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return files;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Rollback all tracked operations in reverse order
|
|
154
|
+
*/
|
|
155
|
+
async rollback() {
|
|
156
|
+
const errors = [];
|
|
157
|
+
|
|
158
|
+
// Reverse order for rollback
|
|
159
|
+
for (const op of this.operations.reverse()) {
|
|
160
|
+
try {
|
|
161
|
+
switch (op.type) {
|
|
162
|
+
case 'create':
|
|
163
|
+
// Delete created file
|
|
164
|
+
if (await fs.pathExists(op.path)) {
|
|
165
|
+
await fs.unlink(op.path);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'update':
|
|
170
|
+
// Restore from backup
|
|
171
|
+
if (op.backup && await fs.pathExists(op.backup)) {
|
|
172
|
+
await fs.copy(op.backup, op.path);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'mkdir':
|
|
177
|
+
// Remove empty directory
|
|
178
|
+
if (await fs.pathExists(op.path)) {
|
|
179
|
+
const files = await fs.readdir(op.path);
|
|
180
|
+
if (files.length === 0) {
|
|
181
|
+
await fs.rmdir(op.path);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
errors.push({
|
|
188
|
+
operation: op,
|
|
189
|
+
error: error.message
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.operations = [];
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
success: errors.length === 0,
|
|
198
|
+
errors
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Commit operations (clean up backups)
|
|
204
|
+
*/
|
|
205
|
+
async commit() {
|
|
206
|
+
for (const op of this.operations) {
|
|
207
|
+
if (op.backup && await fs.pathExists(op.backup)) {
|
|
208
|
+
await fs.unlink(op.backup);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.operations = [];
|
|
213
|
+
return {success: true};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clear operation tracking without cleanup
|
|
218
|
+
*/
|
|
219
|
+
clear() {
|
|
220
|
+
this.operations = [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {FileSystemAdapter};
|