Initial version
This commit is contained in:
commit
435412ef72
26
.eslintrc.cjs
Normal file
26
.eslintrc.cjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const globals = require('globals');
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2021,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'warn',
|
||||||
|
'no-console': 'off',
|
||||||
|
'indent': ['error', 2],
|
||||||
|
'quotes': ['error', 'single'],
|
||||||
|
'semi': ['error', 'always'],
|
||||||
|
|
||||||
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
*/node_modules/
|
||||||
|
*/build/
|
||||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 80,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
7
.sequelizerc
Normal file
7
.sequelizerc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const path = require('path');
|
||||||
|
module.exports = {
|
||||||
|
"config": path.resolve("src", "db", "db.config.js"),
|
||||||
|
"models-path": path.resolve("src", "db", "models"),
|
||||||
|
"seeders-path": path.resolve("src", "db", "seeders"),
|
||||||
|
"migrations-path": path.resolve("src", "db", "migrations")
|
||||||
|
};
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
FROM node:20.15.1-alpine
|
||||||
|
|
||||||
|
RUN apk update && apk add bash
|
||||||
|
# Create app directory
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install app dependencies
|
||||||
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||||
|
# where available (npm@5+)
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
# If you are building your code for production
|
||||||
|
# RUN npm ci --only=production
|
||||||
|
|
||||||
|
|
||||||
|
# Bundle app source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
|
||||||
|
EXPOSE 4000
|
||||||
|
|
||||||
|
CMD [ "yarn", "start" ]
|
||||||
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#test - template backend,
|
||||||
|
|
||||||
|
#### Run App on local machine:
|
||||||
|
|
||||||
|
##### Install local dependencies:
|
||||||
|
|
||||||
|
- `yarn install`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
##### Start build:
|
||||||
|
|
||||||
|
- `yarn start`
|
||||||
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "app-shell",
|
||||||
|
"description": "app-shell",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nodemon ./src/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.26.7",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"bcrypt": "5.1.1",
|
||||||
|
"cors": "2.8.5",
|
||||||
|
"csv-parser": "^3.0.0",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"express": "4.18.2",
|
||||||
|
"formidable": "1.2.2",
|
||||||
|
"helmet": "4.1.1",
|
||||||
|
"json2csv": "^5.0.7",
|
||||||
|
"jsonwebtoken": "8.5.1",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"moment": "2.30.1",
|
||||||
|
"multer": "^1.4.4",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-google-oauth2": "^0.2.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-microsoft": "^0.1.0",
|
||||||
|
"postcss": "^8.5.1",
|
||||||
|
"sequelize-json-schema": "^2.1.1",
|
||||||
|
"pg": "^8.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
||||||
|
"@typescript-eslint/parser": "^8.12.2",
|
||||||
|
"cross-env": "7.0.3",
|
||||||
|
"mocha": "8.1.3",
|
||||||
|
"node-mocks-http": "1.9.0",
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
|
"sequelize-cli": "6.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/config.js
Normal file
68
src/config.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
gcloud: {
|
||||||
|
bucket: "fldemo-files",
|
||||||
|
hash: "4b305598e0c1657ad008afb48d376a49"
|
||||||
|
},
|
||||||
|
bcrypt: {
|
||||||
|
saltRounds: 12
|
||||||
|
},
|
||||||
|
admin_pass: "password",
|
||||||
|
admin_email: "admin@flatlogic.com",
|
||||||
|
providers: {
|
||||||
|
LOCAL: 'local',
|
||||||
|
GOOGLE: 'google',
|
||||||
|
MICROSOFT: 'microsoft'
|
||||||
|
},
|
||||||
|
secret_key: 'HUEyqESqgQ1yTwzVlO6wprC9Kf1J1xuA',
|
||||||
|
remote: '',
|
||||||
|
port: process.env.NODE_ENV === "production" ? "" : "8080",
|
||||||
|
hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
|
||||||
|
portUI: process.env.NODE_ENV === "production" ? "" : "3000",
|
||||||
|
|
||||||
|
portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000",
|
||||||
|
|
||||||
|
swaggerUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
|
||||||
|
swaggerPort: process.env.NODE_ENV === "production" ? "" : ":8080",
|
||||||
|
google: {
|
||||||
|
clientId: '671001533244-kf1k1gmp6mnl0r030qmvdu6v36ghmim6.apps.googleusercontent.com',
|
||||||
|
clientSecret: 'Yo4qbKZniqvojzUQ60iKlxqR'
|
||||||
|
},
|
||||||
|
microsoft: {
|
||||||
|
clientId: '4696f457-31af-40de-897c-e00d7d4cff73',
|
||||||
|
clientSecret: 'm8jzZ.5UpHF3=-dXzyxiZ4e[F8OF54@p'
|
||||||
|
},
|
||||||
|
uploadDir: os.tmpdir(),
|
||||||
|
email: {
|
||||||
|
from: 'amana <app@flatlogic.app>',
|
||||||
|
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||||
|
port: 587,
|
||||||
|
auth: {
|
||||||
|
user: 'AKIAVEW7G4PQUBGM52OF',
|
||||||
|
pass: process.env.EMAIL_PASS,
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
|
||||||
|
admin: 'Administrator',
|
||||||
|
user: 'User',
|
||||||
|
},
|
||||||
|
|
||||||
|
project_uuid: '43092204-4784-4dde-93a5-ce98718ee7a5',
|
||||||
|
flHost: process.env.NODE_ENV === 'production' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects',
|
||||||
|
|
||||||
|
};
|
||||||
|
config.pexelsKey = 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
|
||||||
|
config.pexelsQuery = 'gears and cogs in motion';
|
||||||
|
config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost";
|
||||||
|
config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`;
|
||||||
|
config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`;
|
||||||
|
config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`;
|
||||||
|
config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`;
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
23
src/helpers.js
Normal file
23
src/helpers.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
module.exports = class Helpers {
|
||||||
|
static wrapAsync(fn) {
|
||||||
|
return function (req, res, next) {
|
||||||
|
fn(req, res, next).catch(next);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static commonErrorHandler(error, req, res, next) {
|
||||||
|
if ([400, 403, 404].includes(error.code)) {
|
||||||
|
return res.status(error.code).send(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
return res.status(500).send(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static jwtSign(data) {
|
||||||
|
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
|
||||||
|
}
|
||||||
|
};
|
||||||
54
src/index.js
Normal file
54
src/index.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const app = express();
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const checkPermissions = require('./middlewares/check-permissions');
|
||||||
|
const modifyPath = require('./middlewares/modify-path');
|
||||||
|
const VCS = require('./services/vcs');
|
||||||
|
|
||||||
|
const executorRoutes = require('./routes/executor');
|
||||||
|
const vcsRoutes = require('./routes/vcs');
|
||||||
|
|
||||||
|
// Function to initialize the Git repository
|
||||||
|
function initRepo() {
|
||||||
|
const projectId = '27631';
|
||||||
|
return VCS.initRepo(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the Express app on APP_SHELL_PORT (4000)
|
||||||
|
function startServer() {
|
||||||
|
const PORT = 4000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Listening on port ${PORT}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Git check after the server is up
|
||||||
|
function runGitCheck() {
|
||||||
|
initRepo()
|
||||||
|
.then(result => {
|
||||||
|
console.log(result.message);
|
||||||
|
// Here you can add additional logic if needed
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error during repo initialization:', err);
|
||||||
|
// Optionally exit the process if Git check is critical:
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(cors({ origin: true }));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(checkPermissions);
|
||||||
|
app.use(modifyPath);
|
||||||
|
|
||||||
|
app.use('/executor', executorRoutes);
|
||||||
|
app.use('/vcs', vcsRoutes);
|
||||||
|
|
||||||
|
// Start the app_shell server
|
||||||
|
startServer();
|
||||||
|
|
||||||
|
// Now perform Git check
|
||||||
|
runGitCheck();
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
17
src/middlewares/check-permissions.js
Normal file
17
src/middlewares/check-permissions.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
function checkPermissions(req, res, next) {
|
||||||
|
const project_uuid = config.project_uuid;
|
||||||
|
const requiredHeader = 'X-Project-UUID';
|
||||||
|
const headerValue = req.headers[requiredHeader.toLowerCase()];
|
||||||
|
// Logging whatever request we're getting
|
||||||
|
console.log('Request:', req.url, req.method, req.body, req.headers);
|
||||||
|
|
||||||
|
if (headerValue && headerValue === project_uuid) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(403).send({ error: 'Stop right there, criminal scum! Your project UUID is invalid or missing.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = checkPermissions;
|
||||||
8
src/middlewares/modify-path.js
Normal file
8
src/middlewares/modify-path.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
function modifyPath(req, res, next) {
|
||||||
|
if (req.body && req.body.path) {
|
||||||
|
req.body.path = '../../../' + req.body.path;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = modifyPath;
|
||||||
288
src/routes/executor.js
Normal file
288
src/routes/executor.js
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const multer = require('multer');
|
||||||
|
const upload = multer({ dest: 'uploads/' });
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const ExecutorService = require('../services/executor');
|
||||||
|
|
||||||
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/read_project_tree',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path } = req.body;
|
||||||
|
const tree = await ExecutorService.readProjectTree(path);
|
||||||
|
res.status(200).send(tree);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/read_file',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, showLines } = req.body;
|
||||||
|
const content = await ExecutorService.readFileContents(path, showLines);
|
||||||
|
res.status(200).send(content);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/count_file_lines',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path } = req.body;
|
||||||
|
const content = await ExecutorService.countFileLines(path);
|
||||||
|
res.status(200).send(content);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// router.post(
|
||||||
|
// '/read_file_header',
|
||||||
|
// wrapAsync(async (req, res) => {
|
||||||
|
// const { path, N } = req.body;
|
||||||
|
// try {
|
||||||
|
// const header = await ExecutorService.readFileHeader(path, N);
|
||||||
|
// res.status(200).send(header);
|
||||||
|
// } catch (error) {
|
||||||
|
// res.status(500).send({
|
||||||
|
// error: true,
|
||||||
|
// message: error.message,
|
||||||
|
// details: error.details || error.stack,
|
||||||
|
// validation: error.validation
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/read_file_line_context',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, lineNumber, windowSize, showLines } = req.body;
|
||||||
|
try {
|
||||||
|
const context = await ExecutorService.readFileLineContext(path, lineNumber, windowSize, showLines);
|
||||||
|
res.status(200).send(context);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/write_file',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, fileContents, comment } = req.body;
|
||||||
|
try {
|
||||||
|
await ExecutorService.writeFile(path, fileContents, comment);
|
||||||
|
res.status(200).send({ message: 'File written successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/insert_file_content',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, lineNumber, newContent, message } = req.body;
|
||||||
|
try {
|
||||||
|
await ExecutorService.insertFileContent(path, lineNumber, newContent, message);
|
||||||
|
res.status(200).send({ message: 'File written successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/replace_file_line',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, lineNumber, newText } = req.body;
|
||||||
|
try {
|
||||||
|
const result = await ExecutorService.replaceFileLine(path, lineNumber, newText);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/replace_file_chunk',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, startLine, endLine, newCode } = req.body;
|
||||||
|
try {
|
||||||
|
const result = await ExecutorService.replaceFileChunk(path, startLine, endLine, newCode);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/delete_file_lines',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path, startLine, endLine, message } = req.body;
|
||||||
|
try {
|
||||||
|
const result = await ExecutorService.deleteFileLines(path, startLine, endLine, message);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/validate_file',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const { path } = req.body;
|
||||||
|
try {
|
||||||
|
const validationResult = await ExecutorService.validateFile(path);
|
||||||
|
res.status(200).send({ validationResult });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/check_frontend_runtime_error',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await ExecutorService.checkFrontendRuntimeLogs();
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ error: error });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/replace_code_block',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const {path, oldCode, newCode, message} = req.body;
|
||||||
|
try {
|
||||||
|
const response = await ExecutorService.replaceCodeBlock(path, oldCode, newCode, message);
|
||||||
|
res.status(200).send(response);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({
|
||||||
|
error: true,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack,
|
||||||
|
validation: error.validation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post('/update_project_files_from_scheme',
|
||||||
|
upload.single('file'), // 'file' - name of the field in the form
|
||||||
|
async (req, res) => {
|
||||||
|
console.log('Request received');
|
||||||
|
console.log('Headers:', req.headers);
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('File info:', {
|
||||||
|
originalname: req.file.originalname,
|
||||||
|
path: req.file.path,
|
||||||
|
size: req.file.size,
|
||||||
|
mimetype: req.file.mimetype
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starting update process...');
|
||||||
|
const result = await ExecutorService.updateProjectFilesFromScheme(req.file.path);
|
||||||
|
console.log('Update completed, result:', result);
|
||||||
|
|
||||||
|
console.log('Removing temp file...');
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
console.log('Temp file removed');
|
||||||
|
|
||||||
|
console.log('Sending response...');
|
||||||
|
return res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in route handler:', error);
|
||||||
|
if (req.file) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
console.log('Temp file removed after error');
|
||||||
|
} catch (unlinkError) {
|
||||||
|
console.error('Error removing temp file:', unlinkError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error('Update project files error:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: error.message,
|
||||||
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/get_db_schema',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const jsonSchema = await ExecutorService.getDBSchema();
|
||||||
|
res.status(200).send({ jsonSchema });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ error: error });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/execute_sql',
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { query } = req.body;
|
||||||
|
const result = await ExecutorService.executeSQL(query);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ error: error });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
40
src/routes/vcs.js
Normal file
40
src/routes/vcs.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const wrapAsync = require('../helpers').wrapAsync; // Ваша обёртка для обработки асинхронных маршрутов
|
||||||
|
const VSC = require('../services/vcs');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/init', wrapAsync(async (req, res) => {
|
||||||
|
const result = await VSC.initRepo();
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/commit', wrapAsync(async (req, res) => {
|
||||||
|
const { message, files } = req.body;
|
||||||
|
const result = await VSC.commitChanges(message, files);
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/log', wrapAsync(async (req, res) => {
|
||||||
|
const result = await VSC.getLog();
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/rollback', wrapAsync(async (req, res) => {
|
||||||
|
const { ref } = req.body;
|
||||||
|
// const result = await VSC.checkout(ref);
|
||||||
|
const result = await VSC.revert(ref);
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/sync-to-stable', wrapAsync(async (req, res) => {
|
||||||
|
const result = await VSC.mergeDevIntoMaster();
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/reset-dev', wrapAsync(async (req, res) => {
|
||||||
|
const result = await VSC.resetDevBranch();
|
||||||
|
res.status(200).send(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.use('/', require('../helpers').commonErrorHandler);
|
||||||
|
module.exports = router;
|
||||||
88
src/services/database.js
Normal file
88
src/services/database.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Database.js
|
||||||
|
const { Client } = require('pg');
|
||||||
|
const config = require('../../../backend/src/db/db.config');
|
||||||
|
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const dbConfig = config[env];
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
constructor() {
|
||||||
|
this.client = new Client({
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database,
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect once, reuse the client
|
||||||
|
this.client.connect().catch(err => {
|
||||||
|
console.error('Error connecting to the database:', err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeSQL(query) {
|
||||||
|
try {
|
||||||
|
const result = await this.client.query(query);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
rows: result.rows
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing query:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to fetch simple table/column info from 'information_schema'
|
||||||
|
// (You can expand this to handle constraints, indexes, etc.)
|
||||||
|
async getDBSchema(schemaName = 'public') {
|
||||||
|
try {
|
||||||
|
const tableQuery = `
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = $1
|
||||||
|
AND table_type = 'BASE TABLE'
|
||||||
|
ORDER BY table_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const columnQuery = `
|
||||||
|
SELECT table_name, column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = $1
|
||||||
|
ORDER BY table_name, ordinal_position
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [tablesResult, columnsResult] = await Promise.all([
|
||||||
|
this.client.query(tableQuery, [schemaName]),
|
||||||
|
this.client.query(columnQuery, [schemaName]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build a simple schema object:
|
||||||
|
const tables = tablesResult.rows.map(row => row.table_name);
|
||||||
|
const columnsByTable = {};
|
||||||
|
|
||||||
|
columnsResult.rows.forEach(row => {
|
||||||
|
const { table_name, column_name, data_type, is_nullable } = row;
|
||||||
|
if (!columnsByTable[table_name]) columnsByTable[table_name] = [];
|
||||||
|
columnsByTable[table_name].push({ column_name, data_type, is_nullable });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine tables with their columns
|
||||||
|
return tables.map(table => ({
|
||||||
|
table,
|
||||||
|
columns: columnsByTable[table] || [],
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching schema:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
await this.client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Database();
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.email-header {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.email-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.email-footer {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
text-align: center;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.link-primary {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">Verify your email for {appTitle}!</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>Follow this link to verify your email address.</p>
|
||||||
|
<p>
|
||||||
|
If you didn't ask to verify this address, you can ignore this email.
|
||||||
|
</p>
|
||||||
|
<p><a href="{signupUrl}" class="link-primary">{signupUrl}</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
Thanks,<br />
|
||||||
|
The {appTitle} Team
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.email-header {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.email-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.email-footer {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
text-align: center;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: #fff !important;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">Welcome to {appTitle}!</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>
|
||||||
|
You've been invited to join {appTitle}. Please click the button below
|
||||||
|
to set up your account.
|
||||||
|
</p>
|
||||||
|
<a href="{signupUrl}" class="btn-primary">Set up account</a>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
Thanks,<br />
|
||||||
|
The {appTitle} Team
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.email-header {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.email-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.email-footer {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
text-align: center;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.link-primary {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">Reset your password for {appTitle}</div>
|
||||||
|
<div class="email-body">
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>
|
||||||
|
Follow this link to reset your {appTitle} password for your
|
||||||
|
{accountName} account.
|
||||||
|
</p>
|
||||||
|
<p><a href="{resetUrl}" class="link-primary">{resetUrl}</a></p>
|
||||||
|
<p>
|
||||||
|
If you didn't ask to reset your password, you can ignore this email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="email-footer">
|
||||||
|
Thanks,<br />
|
||||||
|
The {appTitle} Team
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
src/services/email/index.js
Normal file
41
src/services/email/index.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const config = require('../../config');
|
||||||
|
const assert = require('assert');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
module.exports = class EmailSender {
|
||||||
|
constructor(email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send() {
|
||||||
|
assert(this.email, 'email is required');
|
||||||
|
assert(this.email.to, 'email.to is required');
|
||||||
|
assert(this.email.subject, 'email.subject is required');
|
||||||
|
assert(this.email.html, 'email.html is required');
|
||||||
|
|
||||||
|
const htmlContent = await this.email.html();
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport(this.transportConfig);
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: this.from,
|
||||||
|
to: this.email.to,
|
||||||
|
subject: this.email.subject,
|
||||||
|
html: htmlContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
return transporter.sendMail(mailOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isConfigured() {
|
||||||
|
return !!config.email?.auth?.pass && !!config.email?.auth?.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
get transportConfig() {
|
||||||
|
return config.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
get from() {
|
||||||
|
return config.email.from;
|
||||||
|
}
|
||||||
|
};
|
||||||
41
src/services/email/list/addressVerification.js
Normal file
41
src/services/email/list/addressVerification.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const { getNotification } = require('../../notifications/helpers');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = class EmailAddressVerificationEmail {
|
||||||
|
constructor(to, link) {
|
||||||
|
this.to = to;
|
||||||
|
this.link = link;
|
||||||
|
}
|
||||||
|
|
||||||
|
get subject() {
|
||||||
|
return getNotification(
|
||||||
|
'emails.emailAddressVerification.subject',
|
||||||
|
getNotification('app.title'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async html() {
|
||||||
|
try {
|
||||||
|
const templatePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../email/htmlTemplates/addressVerification/emailAddressVerification.html',
|
||||||
|
);
|
||||||
|
|
||||||
|
const template = await fs.readFile(templatePath, 'utf8');
|
||||||
|
|
||||||
|
const appTitle = getNotification('app.title');
|
||||||
|
const signupUrl = this.link;
|
||||||
|
|
||||||
|
let html = template
|
||||||
|
.replace(/{appTitle}/g, appTitle)
|
||||||
|
.replace(/{signupUrl}/g, signupUrl)
|
||||||
|
.replace(/{to}/g, this.to);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating invitation email HTML:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
41
src/services/email/list/invitation.js
Normal file
41
src/services/email/list/invitation.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const { getNotification } = require('../../notifications/helpers');
|
||||||
|
|
||||||
|
module.exports = class InvitationEmail {
|
||||||
|
constructor(to, host) {
|
||||||
|
this.to = to;
|
||||||
|
this.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
get subject() {
|
||||||
|
return getNotification(
|
||||||
|
'emails.invitation.subject',
|
||||||
|
getNotification('app.title'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async html() {
|
||||||
|
try {
|
||||||
|
const templatePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../email/htmlTemplates/invitation/invitationTemplate.html',
|
||||||
|
);
|
||||||
|
|
||||||
|
const template = await fs.readFile(templatePath, 'utf8');
|
||||||
|
|
||||||
|
const appTitle = getNotification('app.title');
|
||||||
|
const signupUrl = `${this.host}&invitation=true`;
|
||||||
|
|
||||||
|
let html = template
|
||||||
|
.replace(/{appTitle}/g, appTitle)
|
||||||
|
.replace(/{signupUrl}/g, signupUrl)
|
||||||
|
.replace(/{to}/g, this.to);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating invitation email HTML:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
42
src/services/email/list/passwordReset.js
Normal file
42
src/services/email/list/passwordReset.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const { getNotification } = require('../../notifications/helpers');
|
||||||
|
const path = require('path');
|
||||||
|
const { promises: fs } = require('fs');
|
||||||
|
|
||||||
|
module.exports = class PasswordResetEmail {
|
||||||
|
constructor(to, link) {
|
||||||
|
this.to = to;
|
||||||
|
this.link = link;
|
||||||
|
}
|
||||||
|
|
||||||
|
get subject() {
|
||||||
|
return getNotification(
|
||||||
|
'emails.passwordReset.subject',
|
||||||
|
getNotification('app.title'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async html() {
|
||||||
|
try {
|
||||||
|
const templatePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../email/htmlTemplates/passwordReset/passwordResetEmail.html',
|
||||||
|
);
|
||||||
|
|
||||||
|
const template = await fs.readFile(templatePath, 'utf8');
|
||||||
|
|
||||||
|
const appTitle = getNotification('app.title');
|
||||||
|
const resetUrl = this.link;
|
||||||
|
const accountName = this.to;
|
||||||
|
|
||||||
|
let html = template
|
||||||
|
.replace(/{appTitle}/g, appTitle)
|
||||||
|
.replace(/{resetUrl}/g, resetUrl)
|
||||||
|
.replace(/{accountName}/g, accountName);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating invitation email HTML:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
999
src/services/executor.js
Normal file
999
src/services/executor.js
Normal file
@ -0,0 +1,999 @@
|
|||||||
|
const fs = require('fs').promises;
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const AdmZip = require('adm-zip');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const util = require('util');
|
||||||
|
// Babel Parser for JS/TS/TSX
|
||||||
|
const babelParser = require('@babel/parser');
|
||||||
|
const babelParse = babelParser.parse;
|
||||||
|
|
||||||
|
// Local App DB Connection
|
||||||
|
const database = require('./database');
|
||||||
|
|
||||||
|
// PostCSS for CSS
|
||||||
|
const postcss = require('postcss');
|
||||||
|
|
||||||
|
const execAsync = util.promisify(exec);
|
||||||
|
|
||||||
|
module.exports = class ExecutorService {
|
||||||
|
static async readProjectTree (directoryPath) {
|
||||||
|
const paths = {
|
||||||
|
frontend: '../../../frontend',
|
||||||
|
backend: '../../../backend',
|
||||||
|
default: '../../../'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const publicDir = path.join(__dirname, paths[directoryPath] || directoryPath || paths.default);
|
||||||
|
return await getDirectoryTree(publicDir);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading directory:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async readFileContents(filePath, showLines) {
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
|
||||||
|
if (showLines) {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
const lineObject = {};
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
lineObject[index + 1] = line;
|
||||||
|
});
|
||||||
|
|
||||||
|
return lineObject;
|
||||||
|
} else {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading file:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async countFileLines(filePath) {
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
|
||||||
|
// Check file exists
|
||||||
|
await fs.access(fullPath);
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
|
||||||
|
// Split by newline and count
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
lineCount: lines.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error counting file lines:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// static async readFileHeader(filePath, N = 30) {
|
||||||
|
// try {
|
||||||
|
// const fullPath = path.join(__dirname, filePath);
|
||||||
|
// const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
// const lines = content.split('\n');
|
||||||
|
//
|
||||||
|
// if (lines.length < N) {
|
||||||
|
// return { error: `File has less than ${N} lines` };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const headerLines = lines.slice(0, Math.min(50, lines.length));
|
||||||
|
//
|
||||||
|
// const lineObject = {};
|
||||||
|
// headerLines.forEach((line, index) => {
|
||||||
|
// lineObject[index + 1] = line;
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// return lineObject;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error reading file header:', error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
static async readFileLineContext(filePath, lineNumber, windowSize, showLines) {
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
const start = Math.max(0, lineNumber - windowSize);
|
||||||
|
const end = Math.min(lines.length, lineNumber + windowSize + 1);
|
||||||
|
|
||||||
|
const contextLines = lines.slice(start, end);
|
||||||
|
|
||||||
|
if (showLines) {
|
||||||
|
const lineObject = {};
|
||||||
|
contextLines.forEach((line, index) => {
|
||||||
|
lineObject[start + index + 1] = line;
|
||||||
|
});
|
||||||
|
|
||||||
|
return lineObject;
|
||||||
|
} else {
|
||||||
|
return contextLines.join('\n');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading file line context:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validateFile(filePath) {
|
||||||
|
console.log('Validating file:', filePath);
|
||||||
|
// Read file content
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = await fs.readFile(filePath, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Could not read file: ${filePath}\n${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine file extension
|
||||||
|
let ext = path.extname(filePath).toLowerCase();
|
||||||
|
if (ext === '.temp') {
|
||||||
|
ext = path.extname(filePath.slice(0, -5)).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (ext) {
|
||||||
|
case '.js':
|
||||||
|
case '.ts':
|
||||||
|
case '.tsx': {
|
||||||
|
// Parse JS/TS/TSX with Babel
|
||||||
|
babelParse(content, {
|
||||||
|
sourceType: 'module',
|
||||||
|
// plugins array covers JS, TS, TSX, and optional JS flavors
|
||||||
|
plugins: ['jsx', 'typescript']
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case '.css': {
|
||||||
|
// Parse CSS with PostCSS
|
||||||
|
postcss.parse(content);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// If the extension isn't recognized, assume it's "valid"
|
||||||
|
// or you could throw an error to force a known extension
|
||||||
|
console.warn(`No validation implemented for extension "${ext}". Skipping syntax check.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parsing succeeded, return true
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (parseError) {
|
||||||
|
// Rethrow parse errors with a friendlier message
|
||||||
|
throw parseError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkFrontendRuntimeLogs() {
|
||||||
|
const frontendLogPath = '../frontend/json/runtimeError.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if file exists
|
||||||
|
try {
|
||||||
|
console.log('Accessing frontend logs:', frontendLogPath);
|
||||||
|
await fs.access(frontendLogPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Frontend logs not found:', error);
|
||||||
|
// File doesn't exist - return empty object
|
||||||
|
return { runtime_error: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// File exists, try to read it
|
||||||
|
try {
|
||||||
|
// Read the entire file instead of using tail
|
||||||
|
const fileContent = await fs.readFile(frontendLogPath, 'utf8');
|
||||||
|
console.log('Reading frontend logs:', fileContent);
|
||||||
|
|
||||||
|
// Handle empty file
|
||||||
|
if (!fileContent || fileContent.trim() === '') {
|
||||||
|
return { runtime_error: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON content
|
||||||
|
const runtime_error = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
console.log('Parsed frontend logs:', runtime_error);
|
||||||
|
return { runtime_error };
|
||||||
|
} catch (error) {
|
||||||
|
// Error reading or parsing file
|
||||||
|
console.error('Error reading frontend runtime logs:', error);
|
||||||
|
return { runtime_error: {} };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Unexpected error
|
||||||
|
console.log('Error checking frontend logs:', error);
|
||||||
|
return { runtime_error: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async writeFile(filePath, fileContents, comment) {
|
||||||
|
try {
|
||||||
|
console.log(comment)
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
|
||||||
|
// Write to a temp file first
|
||||||
|
const tempPath = `${fullPath}.temp`;
|
||||||
|
await fs.writeFile(tempPath, fileContents, 'utf8');
|
||||||
|
|
||||||
|
// Validate the temp file
|
||||||
|
await this.validateFile(tempPath);
|
||||||
|
|
||||||
|
// Rename temp file to original path
|
||||||
|
await fs.rename(tempPath, fullPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing file:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async insertFileContent(filePath, lineNumber, newContent, message) {
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
|
||||||
|
// Check file exists
|
||||||
|
await fs.access(fullPath);
|
||||||
|
|
||||||
|
// Read and split by line
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Ensure lineNumber is within [1 ... lines.length + 1]
|
||||||
|
// 1 means "insert at the very first line"
|
||||||
|
// lines.length + 1 means "append at the end"
|
||||||
|
if (lineNumber < 1) {
|
||||||
|
lineNumber = 1;
|
||||||
|
}
|
||||||
|
if (lineNumber > lines.length + 1) {
|
||||||
|
lineNumber = lines.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to 0-based index
|
||||||
|
const insertIndex = lineNumber - 1;
|
||||||
|
|
||||||
|
// Prepare preview
|
||||||
|
const preview = {
|
||||||
|
insertionLine: lineNumber,
|
||||||
|
insertedLines: newContent.split('\n')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert newContent lines at the specified index
|
||||||
|
lines.splice(insertIndex, 0, ...newContent.split('\n'));
|
||||||
|
|
||||||
|
// Write changes to a temp file first
|
||||||
|
const updatedContent = lines.join('\n');
|
||||||
|
const tempPath = `${fullPath}.temp`;
|
||||||
|
await fs.writeFile(tempPath, updatedContent, 'utf8');
|
||||||
|
|
||||||
|
await this.validateFile(tempPath);
|
||||||
|
|
||||||
|
// Rename temp file to original path
|
||||||
|
await fs.rename(tempPath, fullPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error inserting file content:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async replaceFileLine(filePath, lineNumber, newText, message = null) {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
try {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`File not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
if (lineNumber < 1 || lineNumber > lines.length) {
|
||||||
|
throw new Error(`Invalid line number: ${lineNumber}. File has ${lines.length} lines`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof newText !== 'string') {
|
||||||
|
throw new Error('New text must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = {
|
||||||
|
oldLine: lines[lineNumber - 1],
|
||||||
|
newLine: newText,
|
||||||
|
lineNumber: lineNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
lines[lineNumber - 1] = newText;
|
||||||
|
const newContent = lines.join('\n');
|
||||||
|
const tempPath = `${fullPath}.temp`;
|
||||||
|
await fs.writeFile(tempPath, newContent, 'utf8');
|
||||||
|
|
||||||
|
await this.validateFile(tempPath);
|
||||||
|
|
||||||
|
await fs.rename(tempPath, fullPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating file line:', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.unlink(`${fullPath}.temp`);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
error: error,
|
||||||
|
message: error.message,
|
||||||
|
details: error.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async replaceFileChunk(filePath, startLine, endLine, newCode) {
|
||||||
|
try {
|
||||||
|
// Check if this is a single-line change
|
||||||
|
const newCodeLines = newCode.split('\n');
|
||||||
|
if (newCodeLines.length === 1 && endLine === startLine) {
|
||||||
|
// Redirect to replace_file_line
|
||||||
|
return await this.replaceFileLine(filePath, startLine, newCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
try {
|
||||||
|
await fs.access(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`File not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Adjust line numbers to array indices (subtract 1)
|
||||||
|
const startIndex = startLine - 1;
|
||||||
|
const endIndex = endLine - 1;
|
||||||
|
|
||||||
|
// Validate input parameters
|
||||||
|
if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) {
|
||||||
|
throw new Error(`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check type of new code
|
||||||
|
if (typeof newCode !== 'string') {
|
||||||
|
throw new Error('New code must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create changes preview
|
||||||
|
const preview = {
|
||||||
|
oldLines: lines.slice(startIndex, endIndex + 1),
|
||||||
|
newLines: newCode.split('\n'),
|
||||||
|
startLine,
|
||||||
|
endLine
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply changes to temp file first
|
||||||
|
lines.splice(startIndex, endIndex - startIndex + 1, ...newCode.split('\n'));
|
||||||
|
const newContent = lines.join(os.EOL);
|
||||||
|
const tempPath = `${fullPath}.temp`;
|
||||||
|
await fs.writeFile(tempPath, newContent, 'utf8');
|
||||||
|
await this.validateFile(tempPath);
|
||||||
|
// Apply changes if all validations passed
|
||||||
|
await fs.rename(tempPath, fullPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating file slice:', error);
|
||||||
|
|
||||||
|
// Clean up temp file if exists
|
||||||
|
try {
|
||||||
|
await fs.unlink(`${fullPath}.temp`);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
error: error,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async replaceCodeBlock(filePath, oldCode, newCode, message) {
|
||||||
|
try {
|
||||||
|
console.log(message);
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
|
||||||
|
// Check file exists
|
||||||
|
await fs.access(fullPath);
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
let content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
|
||||||
|
// A small helper to unify line breaks to just `\n`
|
||||||
|
const unifyLineBreaks = (str) => str.replace(/\r\n/g, '\n');
|
||||||
|
|
||||||
|
// Normalize line breaks in file content, oldCode, and newCode
|
||||||
|
content = unifyLineBreaks(content);
|
||||||
|
oldCode = unifyLineBreaks(oldCode);
|
||||||
|
newCode = unifyLineBreaks(newCode);
|
||||||
|
|
||||||
|
// Optional: Trim trailing spaces or handle other whitespace normalization if needed
|
||||||
|
// oldCode = oldCode.trim();
|
||||||
|
// newCode = newCode.trim();
|
||||||
|
|
||||||
|
// Check if oldCode actually exists in the content
|
||||||
|
const index = content.indexOf(oldCode);
|
||||||
|
if (index === -1) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Old code not found in file.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a preview before replacing
|
||||||
|
const preview = {
|
||||||
|
oldCodeSnippet: oldCode,
|
||||||
|
newCodeSnippet: newCode
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform replacement (single occurrence). For multiple, use replaceAll or a loop.
|
||||||
|
// If you want a global replacement, consider:
|
||||||
|
// content = content.split(oldCode).join(newCode);
|
||||||
|
content = content.replace(oldCode, newCode);
|
||||||
|
|
||||||
|
// Write to a temp file first
|
||||||
|
const tempPath = `${fullPath}.temp`;
|
||||||
|
await fs.writeFile(tempPath, content, 'utf8');
|
||||||
|
|
||||||
|
await this.validateFile(tempPath);
|
||||||
|
// Rename temp file to original
|
||||||
|
await fs.rename(tempPath, fullPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error replacing code:', error);
|
||||||
|
return {
|
||||||
|
error: error,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//todo add validation
|
||||||
|
static async deleteFileLines(filePath, startLine, endLine, veryShortDescription) {
|
||||||
|
try {
|
||||||
|
const fullPath = path.join(__dirname, filePath);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
await fs.access(fullPath);
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Convert to zero-based indices
|
||||||
|
const startIndex = startLine - 1;
|
||||||
|
const endIndex = endLine - 1;
|
||||||
|
|
||||||
|
// Validate range
|
||||||
|
if (startIndex < 0 || endIndex >= lines.length || startIndex > endIndex) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare a preview of the lines being deleted
|
||||||
|
const preview = {
|
||||||
|
deletedLines: lines.slice(startIndex, endIndex + 1),
|
||||||
|
startLine,
|
||||||
|
endLine
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove lines
|
||||||
|
lines.splice(startIndex, endIndex - startIndex + 1);
|
||||||
|
|
||||||
|
// Join remaining lines and write to a temporary file
|
||||||
|
const newContent = lines.join('\n');
|
||||||
|
const tempPath = `${fullPath}.temp`;
|
||||||
|
await fs.writeFile(tempPath, newContent, 'utf8');
|
||||||
|
|
||||||
|
await this.validateFile(tempPath);
|
||||||
|
// Rename temp file to original
|
||||||
|
await fs.rename(tempPath, fullPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting file lines:', error);
|
||||||
|
return {
|
||||||
|
error: error,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static async validateTypeScript(filePath, content = null) {
|
||||||
|
try {
|
||||||
|
// Basic validation of JSX syntax
|
||||||
|
const jsxErrors = [];
|
||||||
|
|
||||||
|
if (content !== null) {
|
||||||
|
// Check for matching braces
|
||||||
|
if ((content.match(/{/g) || []).length !== (content.match(/}/g) || []).length) {
|
||||||
|
jsxErrors.push("Unmatched curly braces");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid syntax in JSX attributes
|
||||||
|
if (content.includes('label={')) {
|
||||||
|
if (!content.match(/label={[^}]+}/)) {
|
||||||
|
jsxErrors.push("Invalid label attribute syntax");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsxErrors.length > 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: jsxErrors.map(error => ({
|
||||||
|
code: 'JSX_SYNTAX_ERROR',
|
||||||
|
severity: 'error',
|
||||||
|
location: '',
|
||||||
|
message: error
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
errors: [],
|
||||||
|
errorCount: 0,
|
||||||
|
warningCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('TypeScript validation error:', error);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: [{
|
||||||
|
code: 'VALIDATION_FAILED',
|
||||||
|
severity: 'error',
|
||||||
|
location: '',
|
||||||
|
message: `TypeScript validation error: ${error.message}`
|
||||||
|
}],
|
||||||
|
errorCount: 1,
|
||||||
|
warningCount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validateBackendFiles(backendPath) {
|
||||||
|
try {
|
||||||
|
// Check for syntax errors
|
||||||
|
await execAsync(`node --check ${backendPath}/src/index.js`);
|
||||||
|
|
||||||
|
// Try to run the code in a test environment
|
||||||
|
const testProcess = exec(
|
||||||
|
'NODE_ENV=test node -e "try { require(\'./src/index.js\') } catch(e) { console.error(e); process.exit(1) }"',
|
||||||
|
{ cwd: backendPath }
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let output = '';
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
testProcess.stdout.on('data', (data) => {
|
||||||
|
output += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
testProcess.stderr.on('data', (data) => {
|
||||||
|
error += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
testProcess.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ valid: true });
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
valid: false,
|
||||||
|
error: error || output
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout on validation
|
||||||
|
setTimeout(() => {
|
||||||
|
testProcess.kill();
|
||||||
|
resolve({
|
||||||
|
valid: true,
|
||||||
|
warning: 'Validation timeout, but no immediate errors found'
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createBackup(ROOT_PATH) {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const backupDir = path.join(ROOT_PATH, 'backups', timestamp);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir(path.join(ROOT_PATH, 'backups'), { recursive: true });
|
||||||
|
|
||||||
|
const dirsToBackup = ['frontend', 'backend'];
|
||||||
|
|
||||||
|
for (const dir of dirsToBackup) {
|
||||||
|
const sourceDir = path.join(ROOT_PATH, dir);
|
||||||
|
const targetDir = path.join(backupDir, dir);
|
||||||
|
|
||||||
|
await fs.mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
await execAsync(
|
||||||
|
`cd "${sourceDir}" && ` +
|
||||||
|
`find . -type f -not -path "*/node_modules/*" -not -path "*/\\.*" | ` +
|
||||||
|
`while read file; do ` +
|
||||||
|
`mkdir -p "${targetDir}/$(dirname "$file")" && ` +
|
||||||
|
`cp "$file" "${targetDir}/$file"; ` +
|
||||||
|
`done`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Backup created at:', backupDir);
|
||||||
|
return backupDir;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating backup:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async restoreFromBackup(backupDir, ROOT_PATH) {
|
||||||
|
try {
|
||||||
|
console.log('Restoring from backup:', backupDir);
|
||||||
|
await execAsync(`rm -rf ${ROOT_PATH}/backend/*`);
|
||||||
|
await execAsync(`cp -r ${backupDir}/* ${ROOT_PATH}/backend/`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restoring from backup:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateProjectFilesFromScheme(zipFilePath) {
|
||||||
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||||
|
const ROOT_PATH = path.join(__dirname, '../../../');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Checking file access...');
|
||||||
|
await fs.access(zipFilePath);
|
||||||
|
|
||||||
|
console.log('Getting file stats...');
|
||||||
|
const stats = await fs.stat(zipFilePath);
|
||||||
|
console.log('File size:', stats.size);
|
||||||
|
|
||||||
|
if (stats.size > MAX_FILE_SIZE) {
|
||||||
|
console.log('File size exceeds limit');
|
||||||
|
return { success: false, error: 'File size exceeds limit' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copying zip file to /tmp
|
||||||
|
const tempZipPath = path.join('/tmp', path.basename(zipFilePath));
|
||||||
|
await fs.copyFile(zipFilePath, tempZipPath);
|
||||||
|
|
||||||
|
// Launching background update process
|
||||||
|
const servicesUpdate = (async () => {
|
||||||
|
try {
|
||||||
|
console.log('Stopping services...');
|
||||||
|
await stopServices();
|
||||||
|
|
||||||
|
console.log('Creating zip instance...');
|
||||||
|
const zip = new AdmZip(tempZipPath);
|
||||||
|
|
||||||
|
console.log('Extracting files to:', ROOT_PATH);
|
||||||
|
zip.extractAllTo(ROOT_PATH, true);
|
||||||
|
console.log('Files extracted');
|
||||||
|
|
||||||
|
const removedFilesPath = path.join(ROOT_PATH, 'removed_files.json');
|
||||||
|
try {
|
||||||
|
await fs.access(removedFilesPath);
|
||||||
|
const removedFilesContent = await fs.readFile(removedFilesPath, 'utf8');
|
||||||
|
const filesToRemove = JSON.parse(removedFilesContent);
|
||||||
|
await removeFiles(filesToRemove, ROOT_PATH);
|
||||||
|
|
||||||
|
await fs.unlink(removedFilesPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No removed files to process or error accessing removed_files.json:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove temp zip file
|
||||||
|
await fs.unlink(tempZipPath);
|
||||||
|
|
||||||
|
// Start services after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
startServices()
|
||||||
|
.then(() => console.log('Services started successfully'))
|
||||||
|
.catch(e => console.error('Failed to start services:', e));
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in service update process:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
servicesUpdate.catch(error => {
|
||||||
|
console.error('Background update process failed:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Returning immediate response');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Update process initiated'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Critical error in updateProjectFilesFromScheme:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getDBSchema() {
|
||||||
|
try {
|
||||||
|
return await database.getDBSchema();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading schema:', error);
|
||||||
|
throw {
|
||||||
|
error: error,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async executeSQL(query) {
|
||||||
|
try {
|
||||||
|
return await database.executeSQL(query);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error executing query:', error);
|
||||||
|
throw {
|
||||||
|
error: error,
|
||||||
|
message: error.message,
|
||||||
|
details: error.details || error.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getDirectoryTree(dirPath) {
|
||||||
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory() && (
|
||||||
|
entry.name === 'node_modules' ||
|
||||||
|
entry.name === 'app-shell' ||
|
||||||
|
entry.name === '.git' ||
|
||||||
|
entry.name === '.idea'
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = fullPath.replace('/app', '');
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const subTree = await getDirectoryTree(fullPath);
|
||||||
|
Object.keys(subTree).forEach(key => {
|
||||||
|
result[key.replace('/app', '')] = subTree[key];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const fileContent = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const lineCount = fileContent.split('\n').length;
|
||||||
|
result[relativePath] = lineCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopServices() {
|
||||||
|
try {
|
||||||
|
console.log('Finding service processes...');
|
||||||
|
|
||||||
|
// Frontend stopping
|
||||||
|
const { stdout: frontendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ext-server' | awk '{print $1}'");
|
||||||
|
if (frontendProcess.trim()) {
|
||||||
|
console.log('Stopping frontend, pid:', frontendProcess.trim());
|
||||||
|
await execAsync(`kill -15 ${frontendProcess.trim()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend stopping
|
||||||
|
const { stdout: backendProcess } = await execAsync("ps -o pid,cmd | grep '[n]ode ./src/index.js' | grep -v app-shell | awk '{print $1}'");
|
||||||
|
if (backendProcess.trim()) {
|
||||||
|
console.log('Stopping backend, pid:', backendProcess.trim());
|
||||||
|
await execAsync(`kill -15 ${backendProcess.trim()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping services:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startServices() {
|
||||||
|
try {
|
||||||
|
console.log('Starting services...');
|
||||||
|
|
||||||
|
await execAsync('yarn --cwd /app/frontend dev &');
|
||||||
|
await execAsync('yarn --cwd /app/backend start &');
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting services:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkStatus() {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('ps aux');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
frontendRunning: stdout.includes('next-server'),
|
||||||
|
backendRunning: stdout.includes('nodemon') && stdout.includes('/app/backend'),
|
||||||
|
nginxRunning: stdout.includes('nginx: master process')
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateJSXSyntax(code) {
|
||||||
|
// Define validation rules for JSX
|
||||||
|
const rules = [
|
||||||
|
{
|
||||||
|
// JSX attribute with expression
|
||||||
|
pattern: /^[a-zA-Z][a-zA-Z0-9]*={.*}$/,
|
||||||
|
message: 'Invalid JSX attribute syntax'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Invalid sequences
|
||||||
|
pattern: /,{2,}/,
|
||||||
|
message: 'Invalid character sequence detected',
|
||||||
|
shouldNotMatch: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Ternary expressions
|
||||||
|
pattern: /^[a-zA-Z][a-zA-Z0-9]*={[\w\s]+\?[^}]+:[^}]+}$/,
|
||||||
|
message: 'Invalid ternary expression in JSX'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Validate each line
|
||||||
|
const lines = code.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (!trimmedLine) continue;
|
||||||
|
|
||||||
|
// Check each rule
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.shouldNotMatch) {
|
||||||
|
// For patterns that should not be present
|
||||||
|
if (rule.pattern.test(trimmedLine)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: [{
|
||||||
|
code: 'JSX_SYNTAX_ERROR',
|
||||||
|
severity: 'error',
|
||||||
|
location: '',
|
||||||
|
message: rule.message
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For patterns that should match
|
||||||
|
if (trimmedLine.includes('=') && !rule.pattern.test(trimmedLine)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: [{
|
||||||
|
code: 'JSX_SYNTAX_ERROR',
|
||||||
|
severity: 'error',
|
||||||
|
location: '',
|
||||||
|
message: rule.message
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional JSX-specific checks
|
||||||
|
if ((trimmedLine.match(/{/g) || []).length !== (trimmedLine.match(/}/g) || []).length) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: [{
|
||||||
|
code: 'JSX_SYNTAX_ERROR',
|
||||||
|
severity: 'error',
|
||||||
|
location: '',
|
||||||
|
message: 'Unmatched curly braces in JSX'
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all checks pass
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFiles(files, rootPath) {
|
||||||
|
try {
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(rootPath, file);
|
||||||
|
try {
|
||||||
|
await fs.unlink(fullPath);
|
||||||
|
console.log(`File removed: ${fullPath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error when trying to delete a file ${fullPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing files:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/services/notifications/errors/forbidden.js
Normal file
16
src/services/notifications/errors/forbidden.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const { getNotification, isNotification } = require('../helpers');
|
||||||
|
|
||||||
|
module.exports = class ForbiddenError extends Error {
|
||||||
|
constructor(messageCode) {
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (messageCode && isNotification(messageCode)) {
|
||||||
|
message = getNotification(messageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
message = message || getNotification('errors.forbidden.message');
|
||||||
|
|
||||||
|
super(message);
|
||||||
|
this.code = 403;
|
||||||
|
}
|
||||||
|
};
|
||||||
16
src/services/notifications/errors/validation.js
Normal file
16
src/services/notifications/errors/validation.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const { getNotification, isNotification } = require('../helpers');
|
||||||
|
|
||||||
|
module.exports = class ValidationError extends Error {
|
||||||
|
constructor(messageCode) {
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (messageCode && isNotification(messageCode)) {
|
||||||
|
message = getNotification(messageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
message = message || getNotification('errors.validation.message');
|
||||||
|
|
||||||
|
super(message);
|
||||||
|
this.code = 400;
|
||||||
|
}
|
||||||
|
};
|
||||||
30
src/services/notifications/helpers.js
Normal file
30
src/services/notifications/helpers.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const _get = require('lodash/get');
|
||||||
|
const errors = require('./list');
|
||||||
|
|
||||||
|
function format(message, args) {
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.replace(/{(\d+)}/g, function (match, number) {
|
||||||
|
return typeof args[number] != 'undefined' ? args[number] : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNotification = (key) => {
|
||||||
|
const message = _get(errors, key);
|
||||||
|
return !!message;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotification = (key, ...args) => {
|
||||||
|
const message = _get(errors, key);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return format(message, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getNotification = getNotification;
|
||||||
|
exports.isNotification = isNotification;
|
||||||
100
src/services/notifications/list.js
Normal file
100
src/services/notifications/list.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
const errors = {
|
||||||
|
app: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
userDisabled: 'Your account is disabled',
|
||||||
|
forbidden: 'Forbidden',
|
||||||
|
unauthorized: 'Unauthorized',
|
||||||
|
userNotFound: `Sorry, we don't recognize your credentials`,
|
||||||
|
wrongPassword: `Sorry, we don't recognize your credentials`,
|
||||||
|
weakPassword: 'This password is too weak',
|
||||||
|
emailAlreadyInUse: 'Email is already in use',
|
||||||
|
invalidEmail: 'Please provide a valid email',
|
||||||
|
passwordReset: {
|
||||||
|
invalidToken: 'Password reset link is invalid or has expired',
|
||||||
|
error: `Email not recognized`,
|
||||||
|
},
|
||||||
|
passwordUpdate: {
|
||||||
|
samePassword: `You can't use the same password. Please create new password`,
|
||||||
|
},
|
||||||
|
userNotVerified: `Sorry, your email has not been verified yet`,
|
||||||
|
emailAddressVerificationEmail: {
|
||||||
|
invalidToken: 'Email verification link is invalid or has expired',
|
||||||
|
error: `Email not recognized`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
iam: {
|
||||||
|
errors: {
|
||||||
|
userAlreadyExists: 'User with this email already exists',
|
||||||
|
userNotFound: 'User not found',
|
||||||
|
disablingHimself: `You can't disable yourself`,
|
||||||
|
revokingOwnPermission: `You can't revoke your own owner permission`,
|
||||||
|
deletingHimself: `You can't delete yourself`,
|
||||||
|
emailRequired: 'Email is required',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
importer: {
|
||||||
|
errors: {
|
||||||
|
invalidFileEmpty: 'The file is empty',
|
||||||
|
invalidFileExcel: 'Only excel (.xlsx) files are allowed',
|
||||||
|
invalidFileUpload:
|
||||||
|
'Invalid file. Make sure you are using the last version of the template.',
|
||||||
|
importHashRequired: 'Import hash is required',
|
||||||
|
importHashExistent: 'Data has already been imported',
|
||||||
|
userEmailMissing: 'Some items in the CSV do not have an email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
forbidden: {
|
||||||
|
message: 'Forbidden',
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
message: 'An error occurred',
|
||||||
|
},
|
||||||
|
searchQueryRequired: {
|
||||||
|
message: 'Search query is required',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emails: {
|
||||||
|
invitation: {
|
||||||
|
subject: `You've been invited to {0}`,
|
||||||
|
body: `
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>You've been invited to {0} set password for your {1} account.</p>
|
||||||
|
<p><a href='{2}'>{2}</a></p>
|
||||||
|
<p>Thanks,</p>
|
||||||
|
<p>Your {0} team</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
emailAddressVerification: {
|
||||||
|
subject: `Verify your email for {0}`,
|
||||||
|
body: `
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>Follow this link to verify your email address.</p>
|
||||||
|
<p><a href='{0}'>{0}</a></p>
|
||||||
|
<p>If you didn't ask to verify this address, you can ignore this email.</p>
|
||||||
|
<p>Thanks,</p>
|
||||||
|
<p>Your {1} team</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
passwordReset: {
|
||||||
|
subject: `Reset your password for {0}`,
|
||||||
|
body: `
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>Follow this link to reset your {0} password for your {1} account.</p>
|
||||||
|
<p><a href='{2}'>{2}</a></p>
|
||||||
|
<p>If you didn't ask to reset your password, you can ignore this email.</p>
|
||||||
|
<p>Thanks,</p>
|
||||||
|
<p>Your {0} team</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = errors;
|
||||||
392
src/services/vcs.js
Normal file
392
src/services/vcs.js
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
const util = require('util');
|
||||||
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
const path = require('path');
|
||||||
|
const { promises: fs } = require("fs");
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const ROOT_PATH = './';
|
||||||
|
const MAX_BUFFER = 1024 * 1024 * 50;
|
||||||
|
const GITEA_DOMAIN = 'gitea.flatlogic.app';
|
||||||
|
const USERNAME = 'admin';
|
||||||
|
const API_TOKEN = 'f22e83489657f49c320d081fc934e5c9daacfa08';
|
||||||
|
|
||||||
|
class VCS {
|
||||||
|
// Main method – controller of the repository initialization process
|
||||||
|
static async initRepo(projectId = 'test') {
|
||||||
|
try {
|
||||||
|
// 1. Ensure the remote repository exists (create if needed)
|
||||||
|
const remoteUrl = await this.setupRemote(projectId);
|
||||||
|
|
||||||
|
// 2. Set up the local repository (initialization, fetching and reset)
|
||||||
|
await this.setupLocalRepo(remoteUrl);
|
||||||
|
|
||||||
|
console.log(`[DEBUG] Repository "${projectId}" is ready (remote code applied).`);
|
||||||
|
return { message: `Repository ${projectId} is ready (remote code applied).` };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error during repo initialization: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks for the existence of the remote repo and creates it if it doesn't exist
|
||||||
|
static async setupRemote(projectId) {
|
||||||
|
console.log(`[DEBUG] Checking remote repository "${projectId}"...`);
|
||||||
|
let repoData = await this.checkRepoExists(projectId);
|
||||||
|
if (!repoData) {
|
||||||
|
console.log(`[DEBUG] Remote repository "${projectId}" does not exist. Creating...`);
|
||||||
|
repoData = await this.createRemoteRepo(projectId);
|
||||||
|
console.log(`[DEBUG] Remote repository created: ${JSON.stringify(repoData)}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[DEBUG] Remote repository "${projectId}" already exists.`);
|
||||||
|
}
|
||||||
|
// Return the URL with token authentication
|
||||||
|
return `https://${USERNAME}:${API_TOKEN}@${GITEA_DOMAIN}/${USERNAME}/${projectId}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the local repository: either fetches/reset if .git exists,
|
||||||
|
// initializes git in a non-empty directory, or clones the repository if empty.
|
||||||
|
static async setupLocalRepo(remoteUrl) {
|
||||||
|
const gitDir = path.join(ROOT_PATH, '.git');
|
||||||
|
const localRepoExists = await this.exists(gitDir);
|
||||||
|
if (localRepoExists) {
|
||||||
|
await this.fetchAndResetRepo();
|
||||||
|
} else {
|
||||||
|
const files = await fs.readdir(ROOT_PATH);
|
||||||
|
if (files.length > 0) {
|
||||||
|
await this.initializeGitRepo(remoteUrl);
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG] Local directory is empty. Cloning remote repository...');
|
||||||
|
await exec(`git clone ${remoteUrl} .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file/directory exists
|
||||||
|
static async exists(pathToCheck) {
|
||||||
|
try {
|
||||||
|
await fs.access(pathToCheck);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the local repository exists, fetches remote data and resets the repository state
|
||||||
|
static async fetchAndResetRepo() {
|
||||||
|
console.log('[DEBUG] Local repository exists. Fetching remote...');
|
||||||
|
await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
const branchReset = await this.tryResetToBranch('fl-dev');
|
||||||
|
|
||||||
|
if (!branchReset) {
|
||||||
|
// If 'fl-dev' branch is not found, try 'master'
|
||||||
|
const masterReset = await this.tryResetToBranch('master');
|
||||||
|
if (masterReset) {
|
||||||
|
// Create 'fl-dev' branch and push it to remote
|
||||||
|
console.log('[DEBUG] Creating and switching to branch "fl-dev"...');
|
||||||
|
await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Pushing fl-dev branch to remote...');
|
||||||
|
await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
} else {
|
||||||
|
// If neither remote master nor fl-dev exist – make an initial commit
|
||||||
|
console.log('[DEBUG] Neither "origin/fl-dev" nor "origin/master" exist. Creating initial commit...');
|
||||||
|
await this.commitInitialChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to check out and reset to the specified branch
|
||||||
|
static async tryResetToBranch(branchName) {
|
||||||
|
try {
|
||||||
|
console.log(`[DEBUG] Checking for remote branch "origin/${branchName}"...`);
|
||||||
|
await exec(`git rev-parse --verify origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log(`[DEBUG] Remote branch "origin/${branchName}" found. Resetting local repository to "origin/${branchName}"...`);
|
||||||
|
await exec(`git reset --hard origin/${branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
await exec(`git checkout ${branchName === 'fl-dev' ? 'fl-dev' : branchName}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[DEBUG] Remote branch "origin/${branchName}" does NOT exist.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If remote branch doesn't exist, make the initial commit and set up branches
|
||||||
|
static async commitInitialChanges() {
|
||||||
|
console.log('[DEBUG] Adding all files for initial commit...');
|
||||||
|
await exec(`git add .`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
const { stdout: status } = await exec(`git status --porcelain`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
if (status.trim()) {
|
||||||
|
await exec(`git commit -m "Initial version"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
await exec(`git push -u origin master --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Creating and switching to branch "fl-dev"...');
|
||||||
|
await exec(`git branch fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Making fl-dev branch identical to master...');
|
||||||
|
await exec(`git reset --hard origin/master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Pushing fl-dev branch to remote...');
|
||||||
|
await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG] No local changes to commit.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the local directory is not empty but .git doesn't exist, initialize git,
|
||||||
|
// add .gitignore, configure the user, and add the remote origin.
|
||||||
|
static async initializeGitRepo(remoteUrl) {
|
||||||
|
console.log('[DEBUG] Local directory is not empty. Initializing git...');
|
||||||
|
const gitignorePath = path.join(ROOT_PATH, '.gitignore');
|
||||||
|
const ignoreContent = `node_modules/\n*/node_modules/\n*/build/\n`;
|
||||||
|
await fs.writeFile(gitignorePath, ignoreContent, 'utf8');
|
||||||
|
|
||||||
|
await exec(`git init`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Configuring git user...');
|
||||||
|
await exec(`git config user.email "support@flatlogic.com"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
await exec(`git config user.name "Flatlogic Bot"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
console.log(`[DEBUG] Adding remote ${remoteUrl}...`);
|
||||||
|
await exec(`git remote add origin ${remoteUrl}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
console.log('[DEBUG] Fetching remote...');
|
||||||
|
await exec(`git fetch origin`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
try {
|
||||||
|
console.log('[DEBUG] Checking for remote branch "origin/fl-dev"...');
|
||||||
|
await exec(`git rev-parse --verify origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Remote branch "origin/fl-dev" exists. Resetting local repository to origin/fl-dev...');
|
||||||
|
await exec(`git reset --hard origin/fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
console.log('[DEBUG] Switching to branch "fl-dev"...');
|
||||||
|
await exec(`git checkout -B fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[DEBUG] Remote branch "origin/fl-dev" does NOT exist. Creating initial commit...');
|
||||||
|
await this.commitInitialChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to check if the repository exists on remote server
|
||||||
|
static async checkRepoExists(repoName) {
|
||||||
|
const url = `https://${GITEA_DOMAIN}/api/v1/repos/${USERNAME}/${repoName}`;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: { Authorization: `token ${API_TOKEN}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response && err.response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Error('Error checking repository existence: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to create a remote repository via API
|
||||||
|
static async createRemoteRepo(repoName) {
|
||||||
|
const createUrl = `https://${GITEA_DOMAIN}/api/v1/user/repos`;
|
||||||
|
console.log("[DEBUG] createUrl", createUrl);
|
||||||
|
try {
|
||||||
|
const response = await axios.post(createUrl, {
|
||||||
|
name: repoName,
|
||||||
|
description: `Repository for project ${repoName}`,
|
||||||
|
private: false
|
||||||
|
}, {
|
||||||
|
headers: { Authorization: `token ${API_TOKEN}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('Error creating repository via API: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async commitChanges(message = "", files = '.') {
|
||||||
|
// Ensure that we are on branch 'fl-dev' before making any commits
|
||||||
|
await this._ensureDevBranch();
|
||||||
|
try {
|
||||||
|
await exec(`git add ${files}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
const { stdout: status } = await exec('git status --porcelain', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
if (!status.trim()) {
|
||||||
|
return { message: "No changes to commit" };
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const commitMessage = message || `Auto commit: ${now.toISOString()}`;
|
||||||
|
console.log('commitMessage:', commitMessage);
|
||||||
|
await exec(`git commit -m "${commitMessage}"`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
await this._pushChanges();
|
||||||
|
console.log('Pushed');
|
||||||
|
return { message: "Changes committed" };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error during commit: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getLog() {
|
||||||
|
try {
|
||||||
|
const { stdout } = await exec('git log fl-dev --oneline', { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== '');
|
||||||
|
const result = {};
|
||||||
|
lines.forEach((line) => {
|
||||||
|
const firstSpaceIndex = line.indexOf(' ');
|
||||||
|
if (firstSpaceIndex > 0) {
|
||||||
|
const hash = line.substring(0, firstSpaceIndex);
|
||||||
|
const message = line.substring(firstSpaceIndex + 1).trim();
|
||||||
|
result[hash] = message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error during get log: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkout(ref) {
|
||||||
|
try {
|
||||||
|
await exec(`git checkout ${ref}`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
return { message: `Checked out to ${ref}` };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error during checkout: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async revert(commitHash) {
|
||||||
|
try {
|
||||||
|
await exec(`git reset --hard`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
// Rollback to the specified commit hash
|
||||||
|
await exec(
|
||||||
|
`git revert --no-edit ${commitHash}..HEAD --no-commit`,
|
||||||
|
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
|
||||||
|
);
|
||||||
|
// Commit the changes
|
||||||
|
await exec(
|
||||||
|
`git commit -m "Revert to version ${commitHash}"`,
|
||||||
|
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
|
||||||
|
);
|
||||||
|
await this._pushChanges();
|
||||||
|
|
||||||
|
return { message: `Reverted to commit ${commitHash}` };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during range revert:", error.message);
|
||||||
|
if (error.stdout) {
|
||||||
|
console.error("Revert stdout:", error.stdout);
|
||||||
|
}
|
||||||
|
if (error.stderr) {
|
||||||
|
console.error("Revert stderr:", error.stderr);
|
||||||
|
}
|
||||||
|
throw new Error(`Error during range revert: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async mergeDevIntoMaster() {
|
||||||
|
try {
|
||||||
|
// Switch to branch 'master'
|
||||||
|
console.log('Switching to branch "master"...');
|
||||||
|
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
// Merge branch 'fl-dev' into 'master' with a forced merge.
|
||||||
|
// Parameter -X theirs is used to resolve conflicts by keeping the changes from the branch being merged in case of conflicts.
|
||||||
|
console.log('Merging branch "fl-dev" into "master" (force merge with -X theirs)...');
|
||||||
|
await exec(
|
||||||
|
`git merge fl-dev --no-ff -X theirs -m "Forced merge: merge fl-dev into master"`,
|
||||||
|
{ cwd: ROOT_PATH, maxBuffer: MAX_BUFFER }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Push the merged 'master' branch to remote
|
||||||
|
console.log('Pushing merged master branch to remote...');
|
||||||
|
const { stdout, stderr } = await exec(`git push origin master`, {
|
||||||
|
cwd: ROOT_PATH,
|
||||||
|
maxBuffer: MAX_BUFFER
|
||||||
|
});
|
||||||
|
if (stdout) {
|
||||||
|
console.log("Git push stdout:", stdout);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
console.error("Git push stderr:", stderr);
|
||||||
|
}
|
||||||
|
return { message: "Branch fl-dev merged into master and pushed to remote" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during mergeDevIntoMaster:", error.message);
|
||||||
|
if (error.stdout) {
|
||||||
|
console.error("Merge stdout:", error.stdout);
|
||||||
|
}
|
||||||
|
if (error.stderr) {
|
||||||
|
console.error("Merge stderr:", error.stderr);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async resetDevBranch() {
|
||||||
|
try {
|
||||||
|
console.log('[DEBUG] Switching to branch "master"...');
|
||||||
|
await exec(`git checkout master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
console.log('[DEBUG] Resetting branch "fl-dev" to be identical to "master"...');
|
||||||
|
// Command checkout -B fl-dev master creates branch 'fl-dev' from 'master' and switches to it
|
||||||
|
await exec(`git checkout -B fl-dev master`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
console.log('[DEBUG] Pushing updated branch "fl-dev" to remote (force push)...');
|
||||||
|
await exec(`git push -u origin fl-dev --force`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
|
||||||
|
return { message: 'fl-dev branch successfully reset to master.' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during resetting fl-dev branch:", error.message);
|
||||||
|
if (error.stdout) {
|
||||||
|
console.error("Reset stdout:", error.stdout);
|
||||||
|
}
|
||||||
|
if (error.stderr) {
|
||||||
|
console.error("Reset stderr:", error.stderr);
|
||||||
|
}
|
||||||
|
throw new Error(`Error during resetting fl-dev branch: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async _pushChanges() {
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await exec(`git push origin fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
if (stdout) {
|
||||||
|
console.log("Git push stdout:", stdout);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
console.error("Git push stderr:", stderr);
|
||||||
|
}
|
||||||
|
return { message: "Changes pushed to remote repository (fl-dev branch)" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Git push error:", error.message);
|
||||||
|
if (error.stdout) {
|
||||||
|
console.error("Git push stdout:", error.stdout);
|
||||||
|
}
|
||||||
|
if (error.stderr) {
|
||||||
|
console.error("Git push stderr:", error.stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async _ensureDevBranch() {
|
||||||
|
try {
|
||||||
|
// Check if branch 'fl-dev' exists
|
||||||
|
const { stdout: branchList } = await exec(`git branch --list fl-dev`, {
|
||||||
|
cwd: ROOT_PATH,
|
||||||
|
maxBuffer: MAX_BUFFER,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!branchList || branchList.trim() === '') {
|
||||||
|
console.log("Branch 'fl-dev' not found. Creating branch 'fl-dev'.");
|
||||||
|
await exec(`git checkout -b fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
} else {
|
||||||
|
// Determine current branch
|
||||||
|
const { stdout: currentBranchStdout } = await exec(`git rev-parse --abbrev-ref HEAD`, {
|
||||||
|
cwd: ROOT_PATH,
|
||||||
|
maxBuffer: MAX_BUFFER,
|
||||||
|
});
|
||||||
|
const currentBranch = currentBranchStdout.trim();
|
||||||
|
if (currentBranch !== 'fl-dev') {
|
||||||
|
console.log(`Switching from branch '${currentBranch}' to 'fl-dev'.`);
|
||||||
|
await exec(`git checkout fl-dev`, { cwd: ROOT_PATH, maxBuffer: MAX_BUFFER });
|
||||||
|
} else {
|
||||||
|
console.log("Already on branch 'fl-dev'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error ensuring branch 'fl-dev':", error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VCS;
|
||||||
Loading…
x
Reference in New Issue
Block a user