@orxataguy/tyr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ import { ShellManager } from './ShellManager.js';
2
+ import { Logger } from '../core/Logger.js';
3
+ import { TyrError } from '../core/TyrError.js';
4
+
5
+ export interface DockerRunOptions {
6
+ image: string;
7
+ name: string;
8
+ port?: string;
9
+ env?: string[];
10
+ }
11
+
12
+ /**
13
+ * @class DockerManager
14
+ * @description Low-level manager for interacting with the Docker Daemon.
15
+ * Allows starting individual containers, checking states, and managing Docker Compose stacks.
16
+ */
17
+ export class DockerManager {
18
+ private shell: ShellManager;
19
+ private logger: Logger;
20
+
21
+ constructor(shell: ShellManager, logger: Logger) {
22
+ this.shell = shell;
23
+ this.logger = logger;
24
+ }
25
+
26
+ /**
27
+ * @method isRunning
28
+ * @description Checks whether the Docker service is active and responding on the host system.
29
+ * @returns {Promise<boolean>} True if Docker is running.
30
+ * @example
31
+ * const active = await docker.isRunning();
32
+ * if (!active) fail('Start Docker first.');
33
+ */
34
+ public async isRunning(): Promise<boolean> {
35
+ try {
36
+ await this.shell.exec('docker info');
37
+ return true;
38
+ } catch (e) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @method run
45
+ * @description Deploys an individual container in detached mode. If it already exists, restarts it.
46
+ * @param {DockerRunOptions} config - Deployment configuration.
47
+ * @example
48
+ * await docker.run({ name: 'my-db', image: 'mongo:latest', port: '27017:27017' });
49
+ */
50
+ public async run({ image, name, port, env = [] }: DockerRunOptions): Promise<void> {
51
+ this.logger.info(`Starting container: ${name} (${image})...`);
52
+ try {
53
+ if (await this.containerExists(name)) {
54
+ this.logger.warn(`Container ${name} already exists. Restarting...`);
55
+ await this.shell.exec(`docker rm -f ${name}`);
56
+ }
57
+
58
+ const envFlags = env.map(e => `-e ${e}`).join(' ');
59
+ const portMapping = port ? `-p ${port}` : '';
60
+ const containerId = await this.shell.exec(`docker run -d --name ${name} ${portMapping} ${envFlags} ${image}`);
61
+ this.logger.success(`Container active. ID: ${containerId.substring(0, 12)}`);
62
+ } catch (e) {
63
+ if (e instanceof TyrError) throw e;
64
+ throw new TyrError(`Could not start container: ${name}`, e, 'Check that Docker is running and the image exists.');
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @method containerExists
70
+ * @description Checks whether a specific container exists (running or stopped).
71
+ * @param {string} name - The container name to look for.
72
+ * @returns {Promise<boolean>} True if it exists.
73
+ * @example
74
+ * if (await docker.containerExists('my-app')) { ... }
75
+ */
76
+ public async containerExists(name: string): Promise<boolean> {
77
+ try {
78
+ await this.shell.exec(`docker inspect ${name}`);
79
+ return true;
80
+ } catch (e) {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @method composeUp
87
+ * @description Starts a full stack using a docker-compose file.
88
+ * @param {string} file - Relative path to the compose file.
89
+ * @example
90
+ * await docker.composeUp('infrastructure/db-compose.yml');
91
+ */
92
+ public async composeUp(file: string = 'docker-compose.yml'): Promise<void> {
93
+ this.logger.info(`Starting stack from ${file}...`);
94
+ try {
95
+ await this.shell.exec(`docker-compose -f ${file} up -d`);
96
+ this.logger.success('Stack deployed successfully.');
97
+ } catch (e) {
98
+ if (e instanceof TyrError) throw e;
99
+ throw new TyrError(`Could not start Docker Compose stack from: ${file}`, e, 'Check that the compose file exists and is valid.');
100
+ }
101
+ }
102
+ }
103
+
104
+ export const DockerManagerTests = {
105
+ // isRunning: {},
106
+ // run: { image: 'alpine:latest', name: 'tyr-test-container', env: [] },
107
+ // containerExists: { name: 'tyr-test-container' },
108
+ };
@@ -0,0 +1,152 @@
1
+ import fs from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import path from 'path';
5
+
6
+ import { Logger } from '../core/Logger.js';
7
+ import { TyrError } from '../core/TyrError.js';
8
+
9
+ /**
10
+ * @class FileSystemManager
11
+ * @description Abstraction layer over the file system (fs).
12
+ * Includes safety utilities such as automatic backups when overwriting and idempotent writes.
13
+ */
14
+ export class FileSystemManager {
15
+ private logger: Logger;
16
+
17
+ constructor(logger: Logger) {
18
+ this.logger = logger;
19
+ }
20
+
21
+ private resolvePath(filePath: string): string {
22
+ return filePath.startsWith('~/')
23
+ ? path.join(homedir(), filePath.slice(2))
24
+ : filePath;
25
+ }
26
+
27
+ /**
28
+ * @method exists
29
+ * @description Synchronously checks whether a file or directory exists at the given path.
30
+ * @param {string} filePath - Relative or absolute path to check.
31
+ * @returns {boolean} True if the file exists.
32
+ * @example
33
+ * if (fs.exists('./config.json')) {
34
+ * logger.info('Config found.');
35
+ * }
36
+ */
37
+ public exists(filePath: string): boolean {
38
+ const resolvedPath = this.resolvePath(filePath);
39
+ return existsSync(resolvedPath);
40
+ }
41
+
42
+ /**
43
+ * @method read
44
+ * @description Reads the content of a file in UTF-8 format. Returns null if the file does not exist.
45
+ * @param {string} filePath - Path to the file.
46
+ * @returns {Promise<string|null>} File content or null if it does not exist.
47
+ * @example
48
+ * const content = await fs.read('.env');
49
+ */
50
+ public async read(filePath: string): Promise<string | null> {
51
+ const resolvedPath = this.resolvePath(filePath);
52
+ try {
53
+ return await fs.readFile(resolvedPath, 'utf-8');
54
+ } catch (e) {
55
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
56
+ throw new TyrError(`Could not read file: ${filePath}`, e, 'Check that the file exists and has read permissions.');
57
+ }
58
+ }
59
+
60
+ /**
61
+ * @method delete
62
+ * @description Deletes a file if it exists.
63
+ * @param {string} filePath - Path to the file to delete.
64
+ * @example
65
+ * await fs.delete('./temp/cache.log');
66
+ */
67
+ public async delete(filePath: string): Promise<void> {
68
+ const resolvedPath = this.resolvePath(filePath);
69
+ if (!this.exists(resolvedPath)) {
70
+ throw new TyrError(`Cannot delete: file not found: ${filePath}`, null, 'Check that the path is correct.');
71
+ }
72
+ try {
73
+ await fs.unlink(resolvedPath);
74
+ this.logger.success(`File deleted: ${filePath}`);
75
+ } catch (e) {
76
+ throw new TyrError(`Could not delete file: ${filePath}`, e);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * @method write
82
+ * @description Writes content to a file. If the file already exists, creates a .bak backup before overwriting.
83
+ * @param {string} filePath - Destination path.
84
+ * @param {string} content - Text content to write.
85
+ * @example
86
+ * await fs.write('src/config.js', 'export const port = 3000;');
87
+ */
88
+ public async write(filePath: string, content: string): Promise<void> {
89
+ const resolvedPath = this.resolvePath(filePath);
90
+ try {
91
+ const dir = path.dirname(resolvedPath);
92
+ await fs.mkdir(dir, { recursive: true });
93
+
94
+ if (this.exists(resolvedPath)) {
95
+ const backupPath = `${resolvedPath}.bak`;
96
+ await fs.copyFile(resolvedPath, backupPath);
97
+ this.logger.info(`Backup created at: ${backupPath}`);
98
+ }
99
+
100
+ await fs.writeFile(resolvedPath, content, 'utf-8');
101
+ this.logger.success(`File written: ${filePath}`);
102
+ } catch (e) {
103
+ if (e instanceof TyrError) throw e;
104
+ throw new TyrError(`Could not write file: ${filePath}`, e, 'Check write permissions on the destination directory.');
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @method createDir
110
+ * @description Creates a directory recursively (like mkdir -p). Does nothing if it already exists.
111
+ * @param {string} dirPath - Path of the directory to create.
112
+ * @example
113
+ * await fs.createDir('src/controllers/api/v1');
114
+ */
115
+ public async createDir(dirPath: string): Promise<void> {
116
+ const resolvedPath = this.resolvePath(dirPath);
117
+ if (this.exists(resolvedPath)) return;
118
+ try {
119
+ await fs.mkdir(resolvedPath, { recursive: true });
120
+ this.logger.info(`Directory created: ${dirPath}`);
121
+ } catch (e) {
122
+ throw new TyrError(`Could not create directory: ${dirPath}`, e, 'Check write permissions on the parent directory.');
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @method ensureLine
128
+ * @description Ensures that a specific line exists in a file. Useful for adding environment variables or config entries without duplicating them.
129
+ * @param {string} filePath - Path to the file.
130
+ * @param {string} line - The exact line to ensure.
131
+ * @example
132
+ * await fs.ensureLine('.env', 'PORT=8080');
133
+ */
134
+ public async ensureLine(filePath: string, line: string): Promise<void> {
135
+ const resolvedPath = this.resolvePath(filePath);
136
+ const content = (await this.read(resolvedPath)) || '';
137
+ if (content.includes(line)) {
138
+ this.logger.info(`Line already present in ${filePath}. Skipping.`);
139
+ return;
140
+ }
141
+ const newContent = content.endsWith('\n') ? content + line : content + '\n' + line;
142
+ await this.write(filePath, newContent);
143
+ }
144
+ }
145
+
146
+ export const FileSystemManagerTests = {
147
+ exists: { filePath: '~/Projects/TyrFramework/package.json' },
148
+ read: { filePath: '~/Projects/TyrFramework/package.json' },
149
+ write: { filePath: '~/Projects/TyrFramework/tests/foo.test.txt', content: 'Test content from TyrFramework' },
150
+ delete: { filePath: '~/Projects/TyrFramework/tests/foo.test.txt' },
151
+ ensureLine: { filePath: '~/Projects/TyrFramework/package.json', line: '"type": "module",' },
152
+ };
@@ -0,0 +1,76 @@
1
+ import { ShellManager } from './ShellManager.js';
2
+ import { Logger } from '../core/Logger.js';
3
+ import { TyrError } from '../core/TyrError.js';
4
+
5
+ /**
6
+ * @class GitManager
7
+ * @description Wrapper for common Git operations. Automates repository initialization, commits and cloning.
8
+ */
9
+ export class GitManager {
10
+ private shell: ShellManager;
11
+ private logger: Logger;
12
+
13
+ constructor(shell: ShellManager, logger: Logger) {
14
+ this.shell = shell;
15
+ this.logger = logger;
16
+ }
17
+
18
+ /**
19
+ * @method init
20
+ * @description Initializes a Git repository in the current directory and renames the default branch to 'main'.
21
+ * @example
22
+ * await git.init();
23
+ */
24
+ public async init(): Promise<void> {
25
+ try { await this.shell.exec('git init'); await this.shell.exec('git branch -M main'); } catch (e) {
26
+ throw new TyrError(`Could not init git repository`, e, 'Check if the current directory still exists.');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @method addAll
32
+ * @description Stages all files in the current directory (git add .).
33
+ * @example
34
+ * await git.addAll();
35
+ */
36
+ public async addAll(): Promise<void> {
37
+ await this.shell.exec('git add .');
38
+ }
39
+
40
+ /**
41
+ * @method commit
42
+ * @description Creates a commit with the provided message.
43
+ * @param {string} message - The commit message.
44
+ * @example
45
+ * await git.commit("feat: initial project structure");
46
+ */
47
+ public async commit(message: string): Promise<void> {
48
+ await this.shell.exec(`git commit -m "${message}"`);
49
+ this.logger.success(`Commit created: "${message}"`);
50
+ }
51
+
52
+ /**
53
+ * @method clone
54
+ * @description Clones a remote repository into the current directory.
55
+ * @param {string} repoUrl - The HTTPS or SSH URL of the repository.
56
+ * @example
57
+ * await git.clone('https://github.com/user/repo.git');
58
+ */
59
+ public async clone(repoUrl: string): Promise<void> {
60
+ this.logger.info(`Cloning ${repoUrl}...`);
61
+ try {
62
+ await this.shell.exec(`git clone ${repoUrl}`);
63
+ } catch (e) {
64
+ throw new TyrError(`Could not find the repository ` + repoUrl, e, 'Check if the repository exists or if you have the right permissions to clone it.');
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @object GitManagerTests
71
+ * @description Test parameters to validate GitManager functionality.
72
+ */
73
+ export const GitManagerTests = {
74
+ init: { directory: '/tmp/tyr-git-test' },
75
+ addAll: { directory: '/tmp/tyr-git-test' },
76
+ };
@@ -0,0 +1,87 @@
1
+ import { ShellManager } from './ShellManager.js';
2
+ import { Logger } from '../core/Logger.js';
3
+ import { TyrError } from '../core/TyrError.js';
4
+
5
+ /**
6
+ * @class PackageManager
7
+ * @description OS-agnostic package manager. Automatically detects whether the system uses apt, brew or dnf and installs native software.
8
+ */
9
+ export class PackageManager {
10
+ private shell: ShellManager;
11
+ private logger: Logger;
12
+ private manager: string | null;
13
+
14
+ constructor(shell: ShellManager, logger: Logger) {
15
+ this.shell = shell;
16
+ this.logger = logger;
17
+ this.manager = null;
18
+ }
19
+
20
+ /**
21
+ * @method detect
22
+ * @description Attempts to identify the package manager installed on the host system.
23
+ * @returns {Promise<string>} The name of the detected binary ('apt', 'brew', 'dnf').
24
+ * @example
25
+ * const mgr = await pkg.detect();
26
+ * logger.info(`Using: ${mgr}`);
27
+ */
28
+ public async detect(): Promise<string> {
29
+ if (this.manager) return this.manager;
30
+
31
+ const isWindows = process.platform === 'win32';
32
+ const checkCmd = isWindows ? 'where' : 'which';
33
+
34
+ const candidates: [string, string][] = isWindows
35
+ ? [['winget', 'winget'], ['choco', 'choco'], ['scoop', 'scoop']]
36
+ : [['apt-get', 'apt'], ['brew', 'brew'], ['dnf', 'dnf']];
37
+
38
+ for (const [bin, name] of candidates) {
39
+ try {
40
+ await this.shell.exec(`${checkCmd} ${bin}`);
41
+ this.manager = name;
42
+ return name;
43
+ } catch (e) {}
44
+ }
45
+
46
+ throw new TyrError(
47
+ 'No supported package manager detected.',
48
+ null,
49
+ isWindows
50
+ ? 'Install winget (Windows Package Manager), Chocolatey, or Scoop.'
51
+ : 'Make sure apt, brew or dnf is installed on your system.'
52
+ );
53
+ }
54
+
55
+ /**
56
+ * @method install
57
+ * @description Installs a system package using the detected package manager.
58
+ * @param {string} packageName - Name of the package to install (e.g. 'nginx', 'python3').
59
+ * @example
60
+ * await pkg.install('nginx');
61
+ */
62
+ public async install(packageName: string): Promise<void> {
63
+ const mgr = await this.detect();
64
+ this.logger.info(`Installing ${packageName} using ${mgr}...`);
65
+
66
+ const commands: Record<string, string> = {
67
+ apt: `sudo apt-get install -y ${packageName}`,
68
+ brew: `brew install ${packageName}`,
69
+ dnf: `sudo dnf install -y ${packageName}`,
70
+ winget: `winget install ${packageName}`,
71
+ choco: `choco install -y ${packageName}`,
72
+ scoop: `scoop install ${packageName}`,
73
+ };
74
+
75
+ try {
76
+ await this.shell.exec(commands[mgr]);
77
+ this.logger.success(`Package ${packageName} installed.`);
78
+ } catch (e) {
79
+ if (e instanceof TyrError) throw e;
80
+ throw new TyrError(`Could not install package: ${packageName}`, e, `Try running the install command manually with ${mgr}.`);
81
+ }
82
+ }
83
+ }
84
+
85
+ export const PackageManagerTests = {
86
+ detect: {},
87
+ };
@@ -0,0 +1,121 @@
1
+ import 'dotenv/config';
2
+ import dotenv from 'dotenv';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import sql, { config as SQLConfig } from 'mssql';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
11
+
12
+ /**
13
+ * @class SQLManager
14
+ * @description Conector con la base de datos SQL Server.
15
+ */
16
+ export class SQLManager {
17
+ private pool!: sql.ConnectionPool;
18
+ private connected = false;
19
+
20
+ constructor() {
21
+ }
22
+
23
+ private async init(): Promise<void> {
24
+ if (!this.connected) {
25
+
26
+ const db_config: SQLConfig = {
27
+ user: process.env.MSSQL_USER,
28
+ password: process.env.MSSQL_PASSWORD,
29
+ server: process.env.MSSQL_SERVER || '',
30
+ database: process.env.MSSQL_DATABASE,
31
+ options: {
32
+ encrypt: false,
33
+ trustServerCertificate: true
34
+ }
35
+ };
36
+
37
+ this.pool = await sql.connect(db_config);
38
+ this.connected = true;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @method select
44
+ * @description Ejecuta un comando SELECT en SQL Server y devuelve el resultado como JSON.
45
+ * @param {string} query - El comando SELECT completo.
46
+ * @returns {Promise<any[]>} Los registros del resultado.
47
+ * @example
48
+ * await dbManager.init();
49
+ * const data = await dbManager.select('SELECT * FROM tabla');
50
+ */
51
+ public async select(query: string): Promise<any[]> {
52
+ await this.init();
53
+
54
+ const result = await this.pool.request().query(query);
55
+
56
+ await this.close();
57
+ return result.recordset;
58
+ }
59
+
60
+ /**
61
+ * @method searchBrokerOnDB
62
+ * @description Busca broker por hostname usando la query encoded.
63
+ * @param {string | URL} url - URL o string para extraer hostname.
64
+ * @returns {Promise<string>} Nombre del broker.
65
+ * @example
66
+ * const broker = await db.searchBrokerOnDB('https://www.foo.com');
67
+ */
68
+ public async searchBrokerOnDB(url: string | URL): Promise<string> {
69
+ let urlString = url.toString();
70
+
71
+ if (!urlString.startsWith("http://") && !urlString.startsWith("https://")) {
72
+ urlString = "https://" + urlString;
73
+ }
74
+ let urlObj = new URL(urlString).hostname;
75
+
76
+ if (urlObj.split('.').length < 3) {
77
+ urlObj = ['www', urlObj].join('.');
78
+ }
79
+
80
+ const isWeb = urlObj.startsWith('www') ||
81
+ urlObj.startsWith('horizon') ||
82
+ urlObj.startsWith('ambiance') ||
83
+ urlObj.startsWith('panorama') ||
84
+ urlObj.startsWith('flow') ||
85
+ urlObj.startsWith('panorama') ||
86
+ urlObj.startsWith('avantio') ||
87
+ urlObj.startsWith('demo');
88
+
89
+
90
+ const query = isWeb ? `SELECT basedir as BROKER from ftpUsers where CONCAT(prefijo, '.', dominio) = '${urlObj}'` : `SELECT LOGIN_DS AS BROKER from CR_CANALVENTAS WHERE WEB_DS = '${urlObj}'`;
91
+
92
+ await this.init();
93
+
94
+ const result = await this.pool.request().query(query);
95
+
96
+ await this.close();
97
+
98
+ if (!result.recordset[0] || !result.recordset[0].BROKER) {
99
+ throw new Error(`No se encontró broker para ${urlObj}`);
100
+ }
101
+
102
+ return result.recordset[0].BROKER as string;
103
+ }
104
+
105
+ private async close(): Promise<void> {
106
+ if (this.connected && this.pool) {
107
+ await this.pool.close();
108
+ this.connected = false;
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * @object SQLManagerTests
115
+ * @description Parámetros de pruebas para validar la funcionalidad de SQLManager.
116
+ */
117
+ export const SQLManagerTests = {
118
+ // init: {},
119
+ // select: { query: 'SELECT 1 as test_value' },
120
+ // connectionPool: { queries: ['SELECT 1 as q1', 'SELECT 2 as q2', 'SELECT 3 as q3'] }
121
+ };
@@ -0,0 +1,118 @@
1
+ import { execa } from 'execa';
2
+ import { resolve } from 'path';
3
+ import { homedir } from 'os';
4
+ import inquirer from 'inquirer';
5
+
6
+ import { TyrError } from '../core/TyrError.js';
7
+
8
+ /**
9
+ * @class ShellManager
10
+ * @description Terminal command executor. Maintains the working directory (CWD) state to chain commands in specific folders.
11
+ */
12
+ export class ShellManager {
13
+ private cwd: string;
14
+
15
+ constructor() {
16
+ this.cwd = process.cwd();
17
+ }
18
+
19
+ /**
20
+ * @method exec
21
+ * @description Executes a command in the system shell and returns the standard output.
22
+ * @param {string} command - The full command to execute.
23
+ * @returns {Promise<string>} The command output (stdout) trimmed of extra whitespace.
24
+ * @example
25
+ * const version = await shell.exec('node -v');
26
+ */
27
+ public async exec(command: string): Promise<string> {
28
+ try {
29
+ const result = await execa(command, { shell: true, cwd: this.cwd });
30
+ return result.stdout.trim();
31
+ } catch (e) {
32
+ throw new TyrError(`An error occurred while executing the command: ${command}`, e);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @method showLoader
38
+ * @description Displays a spinner loader in the terminal.
39
+ * @param {string} message - Informational text to show alongside the spinner.
40
+ * @returns {void}
41
+ * @example
42
+ * shell.showLoader('Loading...');
43
+ */
44
+ public showLoader = (message: string): { stop: () => void } => {
45
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
46
+ let i = 0;
47
+ let stopped = false;
48
+
49
+ const interval = setInterval(() => {
50
+ if (!stopped) {
51
+ process.stdout.write(`\r${frames[i]} ${message}`);
52
+ i = (i + 1) % frames.length;
53
+ }
54
+ }, 80);
55
+
56
+ return {
57
+ stop: () => {
58
+ stopped = true;
59
+ clearInterval(interval);
60
+ process.stdout.write('\r');
61
+ }
62
+ };
63
+ };
64
+
65
+ /**
66
+ * @method input
67
+ * @description Prompts the user for a value via CLI.
68
+ * @param {string} question - Informational text shown as the prompt.
69
+ * @returns {Promise<string>} The value entered by the user.
70
+ * @example
71
+ * const name = await shell.input("What's your name?");
72
+ */
73
+ public async input(question: string): Promise<string> {
74
+ try {
75
+ const result = await inquirer.prompt([
76
+ {
77
+ type: 'input',
78
+ name: 'value',
79
+ message: question,
80
+ },
81
+ ]);
82
+
83
+ return result.value.trim();
84
+ } catch (e) {
85
+ throw new TyrError(`An error occurred while prompting the question: ${question}`, e);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @method cd
91
+ * @description Changes the internal working directory for subsequent commands executed by this instance.
92
+ * @param {string} path - Absolute or relative path to change to.
93
+ * @example
94
+ * shell.cd('./backend');
95
+ * await shell.exec('npm install'); // Runs inside /backend
96
+ */
97
+ public cd(path: string): void {
98
+ let expandedPath = path;
99
+ if (path.startsWith('~/')) {
100
+ expandedPath = path.replace('~', homedir());
101
+ } else if (path === '~') {
102
+ expandedPath = homedir();
103
+ }
104
+
105
+ this.cwd = resolve(this.cwd, expandedPath);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @object ShellManagerTests
111
+ * @description Test parameters to validate ShellManager functionality.
112
+ */
113
+ export const ShellManagerTests = {
114
+ exec: { command: 'node -v' },
115
+ cd: { path: '/tmp' },
116
+ input: { question: 'Enter a test value:' },
117
+ showLoader: { message: 'Loading test...' }
118
+ };