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/README.md b/README.md
index ed03990..6f0d207 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# Test Editor
+# Test Editor 100
## This project was generated by [Flatlogic Platform](https://flatlogic.com).
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/config.js b/app-shell/src/config.js
new file mode 100644
index 0000000..4f9c399
--- /dev/null
+++ b/app-shell/src/config.js
@@ -0,0 +1,42 @@
+
+
+const config = {
+
+ project_uuid: '19b1c1bd-792c-4fae-95df-528206c8b6ab',
+ 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,
+
+ eventTypes: {
+ COMMIT: 'PROJECT_DEV_COMMIT',
+ COMMIT_STARTED: 'COMMIT_STARTED',
+ COMMIT_FAILED: 'COMMIT_FAILED',
+ COMMIT_COMPLETED: 'COMMIT_COMPLETED',
+ REPO_INIT_COMPLETED: 'REPO_INIT_COMPLETED',
+ REPO_INIT_STARTED: 'REPO_INIT_STARTED',
+ REPO_INIT_FAILED: 'REPO_INIT_FAILED',
+ REMOTE_REPO_CREATION_STARTED: 'REMOTE_REPO_CREATION_STARTED',
+ REMOTE_REPO_CREATION_COMPLETED: 'REMOTE_REPO_CREATION_COMPLETED',
+ REMOTE_REPO_CREATION_FAILED: 'REMOTE_REPO_CREATION_FAILED',
+ FETCH_RESET_STARTED: 'FETCH_RESET_STARTED',
+ FETCH_RESET_COMPLETED: 'FETCH_RESET_COMPLETED',
+ RESET_TO_BRANCH_COMPLETED: 'RESET_TO_BRANCH_COMPLETED',
+ RESET_TO_BRANCH_FAILED: 'RESET_TO_BRANCH_FAILED',
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
+ DEPLOYMENT: 'DEPLOYMENT_STATUS',
+ BUILD_ERROR: 'BUILD_ERROR',
+ RUNTIME_ERROR: 'RUNTIME_ERROR',
+ USER_ACTION: 'USER_ACTION',
+ READ_PROJECT_TREE_STARTED: 'READ_PROJECT_TREE_STARTED',
+ READ_PROJECT_TREE_COMPLETED: 'READ_PROJECT_TREE_COMPLETED',
+ READ_PROJECT_TREE_FAILED: 'READ_PROJECT_TREE_FAILED',
+ FILE_VALIDATION_STARTED: 'FILE_VALIDATION_STARTED',
+ FILE_VALIDATION_COMPLETED: 'FILE_VALIDATION_COMPLETED',
+ }
+};
+
+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..1cf8bd4
--- /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 = '30652';
+ 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..b1610f8
--- /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 } = 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;
\ 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