diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..2c83cc6
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+backend/node_modules
+frontend/node_modules
+frontend/build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..339a829
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+*/node_modules/
+**/node_modules/
+*/build/
+**/build/
+.DS_Store
+.env
\ No newline at end of file
diff --git a/app-shell/.eslintrc.cjs b/app-shell/.eslintrc.cjs
new file mode 100644
index 0000000..563d159
--- /dev/null
+++ b/app-shell/.eslintrc.cjs
@@ -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',
+ },
+ },
+];
\ No newline at end of file
diff --git a/app-shell/.prettierrc b/app-shell/.prettierrc
new file mode 100644
index 0000000..bb087f2
--- /dev/null
+++ b/app-shell/.prettierrc
@@ -0,0 +1,11 @@
+{
+ "singleQuote": true,
+ "tabWidth": 2,
+ "printWidth": 80,
+ "trailingComma": "all",
+ "quoteProps": "as-needed",
+ "jsxSingleQuote": true,
+ "bracketSpacing": true,
+ "bracketSameLine": false,
+ "arrowParens": "always"
+}
diff --git a/app-shell/.sequelizerc b/app-shell/.sequelizerc
new file mode 100644
index 0000000..fe89188
--- /dev/null
+++ b/app-shell/.sequelizerc
@@ -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")
+};
\ No newline at end of file
diff --git a/app-shell/Dockerfile b/app-shell/Dockerfile
new file mode 100644
index 0000000..eb79c5d
--- /dev/null
+++ b/app-shell/Dockerfile
@@ -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" ]
diff --git a/app-shell/README.md b/app-shell/README.md
new file mode 100644
index 0000000..c53191f
--- /dev/null
+++ b/app-shell/README.md
@@ -0,0 +1,13 @@
+#test - template backend,
+
+#### Run App on local machine:
+
+##### Install local dependencies:
+
+- `yarn install`
+
+---
+
+##### Start build:
+
+- `yarn start`
diff --git a/app-shell/package.json b/app-shell/package.json
new file mode 100644
index 0000000..e33f634
--- /dev/null
+++ b/app-shell/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "app-shell",
+ "description": "app-shell",
+ "scripts": {
+ "start": "node ./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",
+ "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",
+ "nodemon": "^3.1.7",
+ "sequelize-cli": "6.6.2"
+ }
+}
diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json
new file mode 100644
index 0000000..6e52f1a
--- /dev/null
+++ b/app-shell/src/_schema.json
@@ -0,0 +1,4 @@
+{
+ "Initial version": "{\"iv\":\"cgYtu/qeXkNUlsSf\",\"encryptedData\":\"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\"}",
+ "1.1": "{\"iv\":\"FvMRPY7l/6tip8ah\",\"encryptedData\":\"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\"}"
+}
\ No newline at end of file
diff --git a/app-shell/src/config.js b/app-shell/src/config.js
new file mode 100644
index 0000000..09b0133
--- /dev/null
+++ b/app-shell/src/config.js
@@ -0,0 +1,16 @@
+
+
+const config = {
+ schema_encryption_key: process.env.SCHEMA_ENCRYPTION_KEY || '',
+
+ project_uuid: 'd346520e-34c8-4488-890d-8254d64f8607',
+ flHost: process.env.NODE_ENV === 'production' ? 'https://flatlogic.com/projects' : 'http://localhost:3000/projects',
+
+ gitea_domain: process.env.GITEA_DOMAIN || 'gitea.flatlogic.app',
+ gitea_username: process.env.GITEA_USERNAME || 'admin',
+ gitea_api_token: process.env.GITEA_API_TOKEN || null,
+ github_repo_url: process.env.GITHUB_REPO_URL || null,
+ github_token: process.env.GITHUB_TOKEN || null,
+};
+
+module.exports = config;
diff --git a/app-shell/src/helpers.js b/app-shell/src/helpers.js
new file mode 100644
index 0000000..1d918b5
--- /dev/null
+++ b/app-shell/src/helpers.js
@@ -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' });
+ }
+};
diff --git a/app-shell/src/index.js b/app-shell/src/index.js
new file mode 100644
index 0000000..9672df9
--- /dev/null
+++ b/app-shell/src/index.js
@@ -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 = '30719';
+ 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 ? result.message : result);
+ // 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;
diff --git a/app-shell/src/middlewares/check-permissions.js b/app-shell/src/middlewares/check-permissions.js
new file mode 100644
index 0000000..cc9d90a
--- /dev/null
+++ b/app-shell/src/middlewares/check-permissions.js
@@ -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;
\ No newline at end of file
diff --git a/app-shell/src/middlewares/modify-path.js b/app-shell/src/middlewares/modify-path.js
new file mode 100644
index 0000000..0154280
--- /dev/null
+++ b/app-shell/src/middlewares/modify-path.js
@@ -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;
\ No newline at end of file
diff --git a/app-shell/src/routes/executor.js b/app-shell/src/routes/executor.js
new file mode 100644
index 0000000..588cfff
--- /dev/null
+++ b/app-shell/src/routes/executor.js
@@ -0,0 +1,312 @@
+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.post(
+ '/search_files',
+ wrapAsync(async (req, res) => {
+ try {
+ const { searchStrings } = req.body;
+
+ if (
+ typeof searchStrings !== 'string' &&
+ !(
+ Array.isArray(searchStrings) &&
+ searchStrings.every(item => typeof item === 'string')
+ )
+ ) {
+ return res.status(400).send({ error: 'searchStrings must be a string or an array of strings' });
+ }
+
+ const result = await ExecutorService.searchFiles(searchStrings);
+ res.status(200).send(result);
+ } catch (error) {
+ res.status(500).send({ error: error.message });
+ }
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/app-shell/src/routes/vcs.js b/app-shell/src/routes/vcs.js
new file mode 100644
index 0000000..926498d
--- /dev/null
+++ b/app-shell/src/routes/vcs.js
@@ -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, dev_schema } = req.body;
+ const result = await VSC.commitChanges(message, files, dev_schema);
+ 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;
\ No newline at end of file
diff --git a/app-shell/src/services/database.js b/app-shell/src/services/database.js
new file mode 100644
index 0000000..bf8f3a9
--- /dev/null
+++ b/app-shell/src/services/database.js
@@ -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();
diff --git a/app-shell/src/services/executor.js b/app-shell/src/services/executor.js
new file mode 100644
index 0000000..eecb869
--- /dev/null
+++ b/app-shell/src/services/executor.js
@@ -0,0 +1,1206 @@
+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');
+const ProjectEventsService = require('./project-events');
+const config = require('../config.js');
+// 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 ProjectEventsService.sendEvent('SERVICE_STOP_STARTED', {
+ // message: 'Stopping services',
+ // timestamp: new Date().toISOString()
+ // });
+
+ await stopServices();
+
+ // await ProjectEventsService.sendEvent('SERVICE_STOP_COMPLETED', {
+ // message: 'Services stopped successfully',
+ // timestamp: new Date().toISOString()
+ // });
+
+ 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);
+
+ // await ProjectEventsService.sendEvent('SERVICE_START_STARTED', {
+ // message: 'Starting services',
+ // timestamp: new Date().toISOString()
+ // });
+
+ // Start services after a delay
+ setTimeout(async () => {
+ try {
+ await startServices();
+ console.log('Services started successfully');
+
+ await ProjectEventsService.sendEvent('SERVICE_START_COMPLETED', {
+ message: 'All files have been successfully retrieved and applied.',
+ timestamp: new Date().toISOString()
+ });
+ } catch (e) {
+ console.error('Failed to start services:', e);
+ }
+ }, 3000);
+
+ } 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
+ };
+ }
+ }
+
+ static async stopServices() {
+ return await stopServices();
+ }
+
+ static async startServices() {
+ return await startServices();
+ }
+
+ static async checkServicesStatus() {
+ return await checkStatus();
+ }
+
+ static async searchFiles(searchStrings) {
+ const results = {};
+ const ROOT_PATH = path.join(__dirname, '../../../');
+ const directories = [`${ROOT_PATH}backend/`, `${ROOT_PATH}frontend/`];
+ const excludeDirs = ['node_modules', 'build', 'app_shell'];
+
+ if (!Array.isArray(searchStrings)) {
+ searchStrings = [searchStrings];
+ }
+
+ for (const searchString of searchStrings) {
+ try {
+ for (const directoryPath of directories) {
+ const findCommand = `find '${directoryPath}' -type f ${excludeDirs.map(dir => `-not -path "*/${dir}/*"`).join(' ')} -print | xargs grep -nH -C 1 -e '${searchString}'`;
+
+ try {
+ const { stdout } = await execAsync(findCommand);
+
+ const lines = stdout.trim().split('\n').filter(line => line !== '');
+ const searchResults = {};
+ // searchResults['__raw_lines__'] = lines;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const parts = line.split(':');
+ let filePath = '';
+ let lineNumberStr = '';
+ let content = '';
+ let relativeFilePath = '';
+ let lineNum = null;
+
+ if (parts.length >= 3 && !parts[0].includes('-')) {
+ filePath = parts.shift();
+ lineNumberStr = parts.shift();
+ content = parts.join(':').trim();
+ relativeFilePath = filePath.replace(`${ROOT_PATH}`, '');
+ lineNum = parseInt(lineNumberStr, 10) + 1;
+ } else {
+ content = line.trim();
+ }
+
+ const context = [];
+ if (i > 0 && lines[i - 1].includes(':')) {
+ const prevLineParts = lines[i - 1].split(':');
+ if (prevLineParts.length >= 3 && !prevLineParts[0].includes('-')) {
+ prevLineParts.shift();
+ prevLineParts.shift();
+ context.push(prevLineParts.join(':').trim());
+ } else {
+ context.push(lines[i - 1].trim());
+ }
+ }
+ context.push(content);
+ if (i < lines.length - 1 && lines[i + 1].includes(':')) {
+ const nextLineParts = lines[i + 1].split(':');
+ if (nextLineParts.length >= 3 && !nextLineParts[0].includes('-')) {
+ nextLineParts.shift();
+ nextLineParts.shift();
+ context.push(nextLineParts.join(':').trim());
+ } else {
+ context.push(lines[i + 1].trim());
+ }
+ }
+
+ if (relativeFilePath && !searchResults[relativeFilePath]) {
+ searchResults[relativeFilePath] = [];
+ }
+ if (relativeFilePath) {
+ searchResults[relativeFilePath].push({
+ lineNumber: lineNum,
+ context: context.join('\n'),
+ // __filePathAndLine__: filePath + ':' + lineNumberStr + ':' + content,
+ });
+ }
+ }
+
+ if (!results[searchString]) {
+ results[searchString] = {};
+ }
+ Object.assign(results[searchString], searchResults);
+ } catch (err) {
+ if (!err.message.includes('No such file or directory') && !err.stderr.includes('No such file or directory')) {
+ console.error(`Error using find/grep for "${searchString}" in ${directoryPath}:`, err);
+ }
+ }
+ }
+ } catch (error) {
+ console.error(`Error searching for "${searchString}":`, error);
+ results[searchString] = { error: error.message };
+ }
+ }
+
+ return results;
+ }
+
+}
+
+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...');
+ // await ProjectEventsService.sendEvent('SERVICE_STOP_INITIATED', {
+ // message: 'Initiating service stop',
+ // timestamp: new Date().toISOString()
+ // });
+ // 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 ProjectEventsService.sendEvent('FRONTEND_STOP_STARTED', {
+ // message: `Stopping frontend, pid: ${frontendProcess.trim()}`,
+ // timestamp: new Date().toISOString()
+ // });
+
+ // await execAsync(`kill -15 ${frontendProcess.trim()}`);
+
+ // await ProjectEventsService.sendEvent('FRONTEND_STOP_COMPLETED', {
+ // message: 'Frontend stopped successfully',
+ // timestamp: new Date().toISOString()
+ // });
+ }
+
+ // 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 ProjectEventsService.sendEvent('BACKEND_STOP_STARTED', {
+ // message: `Stopping backend, pid: ${backendProcess.trim()}`,
+ // timestamp: new Date().toISOString()
+ // });
+
+ // await execAsync(`kill -15 ${backendProcess.trim()}`);
+
+ // await ProjectEventsService.sendEvent('BACKEND_STOP_COMPLETED', {
+ // message: 'Backend stopped successfully',
+ // timestamp: new Date().toISOString()
+ // });
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 4000));
+
+
+ // await ProjectEventsService.sendEvent('SERVICE_STOP_COMPLETED', {
+ // message: 'All services stopped successfully',
+ // timestamp: new Date().toISOString()
+ // });
+
+ return { success: true };
+ } catch (error) {
+ console.error('Error stopping services:', error);
+
+ await ProjectEventsService.sendEvent('SERVICE_STOP_FAILED', {
+ message: 'Error stopping services',
+ error: error.message,
+ timestamp: new Date().toISOString()
+ });
+
+ return { success: false, error: error.message };
+ }
+}
+
+async function startServices() {
+ try {
+ console.log('Starting services...');
+ // await ProjectEventsService.sendEvent('SERVICE_START_INITIATED', {
+ // message: 'Initiating service start',
+ // timestamp: new Date().toISOString()
+ // });
+
+ // await ProjectEventsService.sendEvent('FRONTEND_START_STARTED', {
+ // message: 'Starting frontend service',
+ // timestamp: new Date().toISOString()
+ // });
+ // await execAsync('yarn --cwd /app/frontend dev &');
+ // await ProjectEventsService.sendEvent('FRONTEND_START_COMPLETED', {
+ // message: 'Frontend service started successfully',
+ // timestamp: new Date().toISOString()
+ // });
+
+ // await ProjectEventsService.sendEvent('BACKEND_START_STARTED', {
+ // message: 'Starting backend service',
+ // timestamp: new Date().toISOString()
+ // });
+ // await execAsync('yarn --cwd /app/backend start &');
+ // await ProjectEventsService.sendEvent('BACKEND_START_COMPLETED', {
+ // message: 'Backend service started successfully',
+ // timestamp: new Date().toISOString()
+ // });
+
+ // await ProjectEventsService.sendEvent('SERVICE_START_COMPLETED', {
+ // message: 'All services started successfully',
+ // timestamp: new Date().toISOString()
+ // });
+
+ return { success: true };
+ } catch (error) {
+ console.error('Error starting services:', error);
+ await ProjectEventsService.sendEvent('SERVICE_START_FAILED', {
+ message: 'Error starting services',
+ error: error.message,
+ timestamp: new Date().toISOString()
+ });
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/app-shell/src/services/notifications/errors/forbidden.js b/app-shell/src/services/notifications/errors/forbidden.js
new file mode 100644
index 0000000..192fa10
--- /dev/null
+++ b/app-shell/src/services/notifications/errors/forbidden.js
@@ -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;
+ }
+};
diff --git a/app-shell/src/services/notifications/errors/validation.js b/app-shell/src/services/notifications/errors/validation.js
new file mode 100644
index 0000000..464550c
--- /dev/null
+++ b/app-shell/src/services/notifications/errors/validation.js
@@ -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;
+ }
+};
diff --git a/app-shell/src/services/notifications/helpers.js b/app-shell/src/services/notifications/helpers.js
new file mode 100644
index 0000000..1c3a60f
--- /dev/null
+++ b/app-shell/src/services/notifications/helpers.js
@@ -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;
diff --git a/app-shell/src/services/notifications/list.js b/app-shell/src/services/notifications/list.js
new file mode 100644
index 0000000..a0a1613
--- /dev/null
+++ b/app-shell/src/services/notifications/list.js
@@ -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: `
+
Hello,
+ You've been invited to {0} set password for your {1} account.
+ {2}
+ Thanks,
+ Your {0} team
+ `,
+ },
+ emailAddressVerification: {
+ subject: `Verify your email for {0}`,
+ body: `
+ Hello,
+ Follow this link to verify your email address.
+ {0}
+ If you didn't ask to verify this address, you can ignore this email.
+ Thanks,
+ Your {1} team
+ `,
+ },
+ passwordReset: {
+ subject: `Reset your password for {0}`,
+ body: `
+ Hello,
+ Follow this link to reset your {0} password for your {1} account.
+ {2}
+ If you didn't ask to reset your password, you can ignore this email.
+ Thanks,
+ Your {0} team
+ `,
+ },
+ },
+};
+
+module.exports = errors;
diff --git a/app-shell/src/services/project-events.js b/app-shell/src/services/project-events.js
new file mode 100644
index 0000000..dabc32d
--- /dev/null
+++ b/app-shell/src/services/project-events.js
@@ -0,0 +1,67 @@
+const axios = require('axios');
+const config = require('../config.js');
+
+class ProjectEventsService {
+ /**
+ * Sends a project event to the Rails backend
+ *
+ * @param {string} eventType - Type of the event
+ * @param {object} payload - Event payload data
+ * @param {object} options - Additional options
+ * @param {string} [options.conversationId] - Optional conversation ID
+ * @param {boolean} [options.isError=false] - Whether this is an error event
+ * @returns {Promise